﻿const TOKEN_KEY = "sweet-life-token";
const THEME_KEY = "sweet-life-theme";
const PROFILE_CACHE_KEY = "sweet-life-profile";
const MEDIA_BASE = "/uploads";

// \u751c\u871c\u751f\u6d3b\u8bb0\u4e8b\u7c3f\u524d\u7aef\u811a\u672c\uff0c\u8d1f\u8d23\u5404\u6a21\u5757\u7684\u6570\u636e\u52a0\u8f7d\u4e0e\u4ea4\u4e92
const PAGE_SIZES = {
  goals: 6,
  routines: 8,
  life: 6,
  diaries: 5,
  reminders: 6,
  quotes: 6,
  notes: 9,
  album: 9,
};

const GOAL_STATUS_OPTIONS = [
  { value: "PENDING", label: "\u672a\u5f00\u59cb" },
  { value: "IN_PROGRESS", label: "\u8fdb\u884c\u4e2d" },
  { value: "COMPLETED", label: "\u5df2\u5b8c\u6210" },
  { value: "ARCHIVED", label: "\u5df2\u5f52\u6863" },
];

const REMINDER_PRIORITY_OPTIONS = [
  { value: "LOW", label: "\u60a0\u95f2\u63d0\u9192" },
  { value: "MEDIUM", label: "\u666e\u901a\u63d0\u9192" },
  { value: "HIGH", label: "\u91cd\u8981\u63d0\u9192" },
];

const MENUS = [
  { id: "dashboard", label: "\u751c\u871c\u603b\u89c8", subtitle: "\u4eca\u5929\u4e5f\u8981\u8ba4\u771f\u8bb0\u5f55\u5c0f\u786e\u5e78" },
  { id: "goals", label: "\u5c0f\u76ee\u6807\u6e05\u5355", subtitle: "\u8bbe\u5b9a\u5fc3\u613f\uff0c\u4e00\u70b9\u70b9\u9760\u8fd1\u5b83" },
  { id: "routines", label: "\u6bcf\u65e5\u65e5\u7a0b", subtitle: "\u5b89\u6392\u597d\u4e00\u5929\uff0c\u8ba9\u751c\u871c\u6709\u5e8f\u53d1\u751f" },
  { id: "life", label: "\u751f\u6d3b\u77ac\u95f4", subtitle: "\u6536\u85cf\u503c\u5f97\u56de\u5473\u7684\u65e5\u5e38\u7247\u6bb5" },
  { id: "diary", label: "\u751c\u871c\u65e5\u5fd7", subtitle: "\u5199\u4e0b\u5fc3\u60c5\u4e0e\u6545\u4e8b\uff0c\u7559\u4f4f\u6e29\u5ea6" },
  { id: "reminders", label: "\u6e29\u6696\u63d0\u9192", subtitle: "\u522b\u5fd8\u4e86\u90a3\u4e9b\u503c\u5f97\u671f\u5f85\u7684\u5c0f\u4e8b" },
  { id: "quotes", label: "\u6536\u85cf\u8bed\u5f55", subtitle: "\u79ef\u7d2f\u80fd\u91cf\uff0c\u5e38\u770b\u5e38\u65b0" },
  { id: "notes", label: "\u5f69\u8272\u6807\u7b7e", subtitle: "\u5199\u4e0b\u5631\u5490\u4e0e\u7075\u611f\uff0c\u8ba9\u7231\u4e0d\u6f0f\u6389" },
  { id: "album", label: "\u7231\u7684\u76f8\u518c", subtitle: "\u8ba9\u56de\u5fc6\u4f4f\u8fdb\u4f1a\u53d1\u5149\u7684\u76f8\u518c" },
  { id: "analytics", label: "\u751c\u871c\u7edf\u8ba1\u56fe", subtitle: "\u7528\u6570\u636e\u8bb0\u5f55\u4eb2\u5bc6\u8db3\u8ff9" },
  { id: "userAdmin", label: "\u7528\u6237\u7ba1\u7406", subtitle: "\u7ef4\u62a4\u4e24\u4f4d\u7528\u6237\u7684\u8d44\u6599" },
];

const state = {
  token: "",
  activeMenu: "dashboard",
  profile: {
    displayName: "\u751c\u871c\u7528\u6237",
    avatarPath: "",
    email: "",
    themePreference: "pink",
    partnerTagline: "\u8bb0\u5f55\u4f60\u4eec\u7684\u751f\u6d3b\u70b9\u6ef4",
    detail: null,
  },
  summary: {
    greeting: "\u4eca\u5929\u4e5f\u8bf7\u597d\u597d\u8bb0\u5f55\u4f60\u4eec\u7684\u6545\u4e8b\u5440",
    today: new Date().toISOString().slice(0, 10),
    goalCount: 0,
    goalCompleted: 0,
    diaryCount: 0,
    reminderCount: 0,
    cheerMessages: [],
  },
  modules: {
    goals: createListState(PAGE_SIZES.goals),
    routines: createListState(PAGE_SIZES.routines),
    life: createListState(PAGE_SIZES.life),
    diaries: createListState(PAGE_SIZES.diaries),
    reminders: createListState(PAGE_SIZES.reminders),
    quotes: createListState(PAGE_SIZES.quotes),
    notes: createListState(PAGE_SIZES.notes),
    album: createListState(PAGE_SIZES.album),
  },
};

document.addEventListener("click", handlePreviewClick);
document.addEventListener("DOMContentLoaded", initApp);

const modal = createModal();
const toast = createToast();
const previewer = createMediaViewer();


function createListState(size) {
  return {
    page: 1,
    size,
    total: 0,
    items: [],
    loading: false,
    lastLoadedAt: 0,
  };
}

async function initApp() {
  state.token = localStorage.getItem(TOKEN_KEY) || "";
  if (!state.token) {
    window.location.href = "index.html";
    return;
  }

  buildSidebarMenu();
  bindThemeButtons();
  bindGlobalButtons();

  const preferredTheme = localStorage.getItem(THEME_KEY) || state.profile.themePreference || "pink";
  applyTheme(preferredTheme);

  try {
    await Promise.all([loadProfile(), loadSummary()]);
  } catch (error) {
    notify(error.message || "\u521d\u59cb\u5316\u6570\u636e\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5", "error");
  }

  updateSidebar();
  updateHeader();

  document.getElementById("app").hidden = false;
  const overlay = document.getElementById("loadingOverlay");
  if (overlay) overlay.remove();

  changeView("dashboard");
}

function buildSidebarMenu() {
  const menuList = document.getElementById("menuList");
  menuList.innerHTML = MENUS.map((menu) => `<button class="menu-item" data-menu="${menu.id}">${menu.label}</button>`).join("");

  menuList.addEventListener("click", (event) => {
    const button = event.target.closest("button[data-menu]");
    if (!button) return;
    changeView(button.dataset.menu);
  });
}

function bindThemeButtons() {
  const themeSwitch = document.getElementById("themeSwitch");
  themeSwitch.addEventListener("click", (event) => {
    const button = event.target.closest("button[data-theme]");
    if (!button) return;
    const theme = button.dataset.theme;
    applyTheme(theme, true);
    themeSwitch.querySelectorAll(".theme-dot").forEach((dot) => {
      dot.classList.toggle("active", dot.dataset.theme === theme);
    });
  });
}

function bindGlobalButtons() {
  document.getElementById("logoutBtn").addEventListener("click", async () => {
    try {
      await apiRequest("/api/auth/logout", { method: "POST" });
    } catch (_) {
      // ignore logout error
    } finally {
      localStorage.removeItem(TOKEN_KEY);
      localStorage.removeItem(PROFILE_CACHE_KEY);
      window.location.href = "index.html";
    }
  });

  document.getElementById("openProfileBtn").addEventListener("click", () => {
    openProfileEditor();
  });
}

function applyTheme(theme, persist = false) {
  document.body.classList.remove("theme-pink", "theme-lavender", "theme-midnight");
  document.body.classList.add(`theme-${theme}`);
  if (persist) {
    localStorage.setItem(THEME_KEY, theme);
  }
  state.profile.themePreference = theme;
}

async function changeView(menuId) {
  const menu = MENUS.find((item) => item.id === menuId);
  state.activeMenu = menu ? menuId : "dashboard";
  document.querySelectorAll(".menu-item").forEach((button) => {
    button.classList.toggle("active", button.dataset.menu === state.activeMenu);
  });
  await renderCurrentView();
}

async function renderCurrentView() {
  switch (state.activeMenu) {
    case "dashboard":
      await renderDashboard();
      break;
    case "goals":
      await renderGoals();
      break;
    case "routines":
      await renderRoutines();
      break;
    case "life":
      await renderLifeMoments();
      break;
    case "diary":
      await renderDiaries();
      break;
    case "reminders":
      await renderReminders();
      break;
    case "quotes":
      await renderQuotes();
      break;
    case "notes":
      await renderNotes();
      break;
    case "album":
      await renderAlbum();
      break;
    case "analytics":
      await renderAnalytics();
      break;
    case "userAdmin":
      await renderUserAdmin();
      break;
    default:
      await renderDashboard();
  }
}

function updateSidebar() {
  const { displayName, avatarPath, partnerTagline } = state.profile;
  const avatar = document.getElementById("sidebarAvatar");
  avatar.src = avatarPath ? resolveMediaUrl(avatarPath) : "images/placeholder-avatar.svg";
  document.getElementById("sidebarName").textContent = displayName || "\u751c\u871c\u7528\u6237";
  document.getElementById("sidebarTag").textContent = partnerTagline || "\u8bb0\u5f55\u4f60\u4eec\u7684\u751f\u6d3b\u70b9\u6ef4";
}

function updateHeader() {
  const currentMenu = MENUS.find((item) => item.id === state.activeMenu) || MENUS[0];
  document.getElementById("viewTitle").textContent = currentMenu.label;
  document.getElementById("viewSubtitle").textContent = currentMenu.subtitle || state.summary.greeting || "";
  document.getElementById("todayDisplay").textContent = formatDate(state.summary.today);
}

async function loadProfile() {
  try {
    const cached = localStorage.getItem(PROFILE_CACHE_KEY);
    if (cached) {
      const profile = JSON.parse(cached);
      state.profile.displayName = profile.displayName || profile.profile?.partnerName || "\u751c\u871c\u7528\u6237";
      state.profile.avatarPath = profile.avatarPath || "";
      state.profile.email = profile.email || state.profile.email || "";
      state.profile.themePreference = profile.themePreference || "pink";
      state.profile.partnerTagline = profile.profile?.partnerName
        ? `\u548c ${profile.profile.partnerName} \u7684\u751c\u871c\u65e5\u5e38`
        : "\u8bb0\u5f55\u4f60\u4eec\u7684\u751f\u6d3b\u70b9\u6ef4";
      state.profile.detail = profile.profile || state.profile.detail;
    }

    const data = await apiRequest("/api/dashboard/profile");
    if (data) {
      state.profile.displayName = data.displayName || data.profile?.partnerName || state.profile.displayName;
      state.profile.avatarPath = data.avatarPath || state.profile.avatarPath;
      state.profile.email = data.email || state.profile.email || "";
      state.profile.themePreference = data.themePreference || state.profile.themePreference;
      state.profile.partnerTagline = data.profile?.partnerName
        ? `\u548c ${data.profile.partnerName} \u7684\u751c\u871c\u65e5\u5e38`
        : state.profile.partnerTagline;
      state.profile.detail = data.profile || state.profile.detail;
      localStorage.setItem(PROFILE_CACHE_KEY, JSON.stringify(data));
    }
  } catch (error) {
    console.error(error);
    notify(error.message || "\u52a0\u8f7d\u4e2a\u4eba\u8d44\u6599\u5931\u8d25", "error");
  }
}

async function loadSummary() {
  try {
    const data = await apiRequest("/api/dashboard/summary");
    if (!data) return;
    state.summary.greeting = data.greeting || state.summary.greeting;
    state.summary.today = data.today || state.summary.today;
    state.summary.goalCount = data.goalCount ?? state.summary.goalCount;
    state.summary.goalCompleted = data.goalCompleted ?? state.summary.goalCompleted;
    state.summary.diaryCount = data.diaryCount ?? state.summary.diaryCount;
    state.summary.reminderCount = data.reminderCount ?? state.summary.reminderCount;
    state.summary.cheerMessages = Array.isArray(data.cheerMessages) ? data.cheerMessages : [];
  } catch (error) {
    console.error(error);
    notify(error.message || "\u52a0\u8f7d\u4eea\u8868\u76d8\u6570\u636e\u5931\u8d25", "error");
  }
}

async function ensureModuleData(moduleKey, force = false) {
  const module = state.modules[moduleKey];
  if (!module) return;
  const now = Date.now();
  if (!force && (module.loading || (module.lastLoadedAt && now - module.lastLoadedAt < 500))) {
    return;
  }

  module.loading = true;
  try {
    const endpoint = {
      goals: "/api/goals",
      routines: "/api/routines",
      life: "/api/life-records",
      diaries: "/api/diaries",
      reminders: "/api/reminders",
      quotes: "/api/quotes",
      notes: "/api/notes",
      album: "/api/album",
    }[moduleKey];

    if (!endpoint) return;

    const params = {
      page: Math.max(module.page - 1, 0),
      size: module.size,
    };

    const data = await apiRequest(endpoint, { params });
    module.items = data?.items || [];
    module.total = data?.total || 0;
    module.lastLoadedAt = Date.now();
  } catch (error) {
    console.error(error);
    notify(error.message || "\u6570\u636e\u52a0\u8f7d\u5931\u8d25", "error");
  } finally {
    module.loading = false;
  }
}

async function renderDashboard() {
  updateHeader();
  const container = getContentArea();

  await Promise.all([
    ensureModuleData("goals", true),
    ensureModuleData("reminders", true),
    ensureModuleData("life", true),
  ]);

  const goalPreview = state.modules.goals.items.slice(0, 3).map(renderGoalCard).join("");
  const reminderPreview = state.modules.reminders.items.slice(0, 3).map(renderReminderCard).join("");
  const lifePreview = state.modules.life.items.slice(0, 4).map(renderLifeCard).join("");

  container.innerHTML = `
    <section class="dashboard-grid">
      <div class="stat-card">
        <h3>\u76ee\u6807\u6570\u91cf</h3>
        <div class="stat-value">${state.summary.goalCount}</div>
        <div class="stat-note">\u5df2\u5b8c\u6210 ${state.summary.goalCompleted} \u9879</div>
      </div>
      <div class="stat-card">
        <h3>\u751c\u871c\u65e5\u5fd7</h3>
        <div class="stat-value">${state.summary.diaryCount}</div>
        <div class="stat-note">\u628a\u5fc3\u60c5\u4e0e\u6545\u4e8b\u8bb0\u4e0b\u6765</div>
      </div>
      <div class="stat-card">
        <h3>\u6e29\u6696\u63d0\u9192</h3>
        <div class="stat-value">${state.summary.reminderCount}</div>
        <div class="stat-note">\u522b\u8ba9\u91cd\u8981\u65e5\u5b50\u88ab\u9057\u5fd8</div>
      </div>
    </section>

    <section class="card">
      <header class="card-header">
        <div>
          <h2 class="card-title">\u4eca\u65e5\u9f13\u52b1</h2>
          <p class="card-subtitle">\u6765\u81ea\u5fc3\u5e95\u7684\u5c0f\u58f0\u63d0\u9192</p>
        </div>
      </header>
      <ul class="timeline">
        ${
          state.summary.cheerMessages.length
            ? state.summary.cheerMessages.map((msg) => `<li class="timeline-item">${escapeHtml(msg)}</li>`).join("")
            : `<li class="timeline-item">\u5199\u4e0b\u4e00\u53e5\u4e13\u5c5e\u9f13\u52b1\uff0c\u8ba9\u5b83\u5e38\u4f34\u8eab\u4fa7</li>`
        }
      </ul>
    </section>

    <section class="card">
      <header class="card-header">
        <div>
          <h2 class="card-title">\u5c0f\u76ee\u6807\u901f\u89c8</h2>
          <p class="card-subtitle">\u4e00\u8d77\u770b\u770b\u6700\u8fd1\u7684\u8ba1\u5212</p>
        </div>
        <button class="btn-link" data-action="goto" data-menu="goals">\u524d\u5f80\u7ba1\u7406</button>
      </header>
      <div class="list-group">
        ${goalPreview || `<div class="empty">\u8fd8\u6ca1\u6709\u76ee\u6807\uff0c\u5feb\u53bb\u521b\u5efa\u4e00\u4e2a\u5427</div>`}
      </div>
    </section>

    <section class="card">
      <header class="card-header">
        <div>
          <h2 class="card-title">\u6e29\u6696\u63d0\u9192</h2>
          <p class="card-subtitle">\u522b\u8ba9\u91cd\u8981\u7684\u5c0f\u4e8b\u88ab\u9057\u5fd8</p>
        </div>
        <button class="btn-link" data-action="goto" data-menu="reminders">\u67e5\u770b\u66f4\u591a</button>
      </header>
      <div class="list-group">
        ${reminderPreview || `<div class="empty">\u6682\u65f6\u6ca1\u6709\u63d0\u9192\uff0c\u5feb\u53bb\u6dfb\u52a0\u4e00\u4e2a\u5427</div>`}
      </div>
    </section>

    <section class="card">
      <header class="card-header">
        <div>
          <h2 class="card-title">\u751f\u6d3b\u77ac\u95f4</h2>
          <p class="card-subtitle">\u73cd\u60dc\u773c\u524d\u7684\u5149\u8292</p>
        </div>
        <button class="btn-link" data-action="goto" data-menu="life">\u67e5\u770b\u66f4\u591a</button>
      </header>
      <div class="media-grid">
        ${lifePreview || `<div class="empty">\u8bb0\u5f55\u4e0b\u4e00\u4e2a\u6696\u5fc3\u77ac\u95f4\u5427</div>`}
      </div>
    </section>
  `;

  container.querySelectorAll("[data-action='goto']").forEach((button) => {
    button.addEventListener("click", () => changeView(button.dataset.menu));
  });
}

async function renderGoals() {
  const module = state.modules.goals;
  await ensureModuleData("goals", true);
  updateHeader();

  const container = getContentArea();
  container.innerHTML = `
    <section class="card">
      <header class="card-header">
        <div>
          <h2 class="card-title">\u5c0f\u76ee\u6807\u6e05\u5355</h2>
          <p class="card-subtitle">\u8bbe\u5b9a\u5fc3\u613f\uff0c\u4e00\u70b9\u70b9\u9760\u8fd1\u5b83</p>
        </div>
        <div class="action-group">
          <button class="btn-secondary" id="createGoal">\u65b0\u589e\u76ee\u6807</button>
          <button class="btn-link" id="refreshGoals">\u5237\u65b0</button>
        </div>
      </header>
      <div class="list-group" id="goalList">
        ${
          module.items.length
            ? module.items.map(renderGoalCard).join("")
            : `<div class="empty">\u6682\u65f6\u6ca1\u6709\u76ee\u6807\uff0c\u53bb\u6dfb\u52a0\u51e0\u4e2a\u5427</div>`
        }
      </div>
      ${renderPagination(module)}
    </section>
  `;

  container.querySelector("#createGoal").addEventListener("click", () => openGoalForm());
  container.querySelector("#refreshGoals").addEventListener("click", () => renderGoals());

  const list = container.querySelector("#goalList");
  list.addEventListener("click", (event) => {
    const actionBtn = event.target.closest("[data-action]");
    if (!actionBtn) return;
    const wrapper = actionBtn.closest("[data-id]");
    if (!wrapper) return;
    const id = wrapper.dataset.id;
    const goal = module.items.find((item) => String(item.id) === id);
    if (!goal) return;
    const action = actionBtn.dataset.action;
    if (action === "edit") {
      openGoalForm(goal);
    } else if (action === "delete") {
      deleteGoal(goal);
    }
  });

  bindPaginationControls(container, module, renderGoals);
}

async function renderRoutines() {
  const module = state.modules.routines;
  await ensureModuleData("routines", true);
  updateHeader();

  const items = module.items.map(renderRoutineItem).join("");

  const container = getContentArea();
  container.innerHTML = `
    <section class="card">
      <header class="card-header">
        <div>
          <h2 class="card-title">\u6bcf\u65e5\u65e5\u7a0b</h2>
          <p class="card-subtitle">\u628a\u4e00\u5929\u5b89\u6392\u5f97\u521a\u521a\u597d\uff0c\u8ba9\u751c\u871c\u6709\u5e8f\u53d1\u751f</p>
        </div>
        <div class="action-group">
          <button class="btn-secondary" id="createRoutine">\u65b0\u589e\u65e5\u7a0b</button>
          <button class="btn-link" id="refreshRoutines">\u5237\u65b0</button>
        </div>
      </header>
      <div class="timeline" id="routineList">
        ${items || `<div class="empty">\u5feb\u6765\u5236\u5b9a\u5c5e\u4e8e\u4f60\u4eec\u7684\u4e00\u5929\u5427</div>`}
      </div>
      ${renderPagination(module)}
    </section>
  `;

  container.querySelector("#createRoutine").addEventListener("click", () => openRoutineForm());
  container.querySelector("#refreshRoutines").addEventListener("click", () => renderRoutines());

  const list = container.querySelector("#routineList");
  list.addEventListener("click", (event) => {
    const actionBtn = event.target.closest("[data-action]");
    if (!actionBtn) return;
    const itemEl = actionBtn.closest("[data-id]");
    if (!itemEl) return;
    const routine = module.items.find((entry) => String(entry.id) === itemEl.dataset.id);
    if (!routine) return;
    const action = actionBtn.dataset.action;
    if (action === "edit") {
      openRoutineForm(routine);
    } else if (action === "delete") {
      deleteRoutine(routine);
    }
  });

  bindPaginationControls(container, module, renderRoutines);
}

async function renderLifeMoments() {
  const module = state.modules.life;
  await ensureModuleData("life", true);
  updateHeader();

  const container = getContentArea();
  container.innerHTML = `
    <section class="card">
      <header class="card-header">
        <div>
          <h2 class="card-title">\u751f\u6d3b\u77ac\u95f4</h2>
          <p class="card-subtitle">\u73cd\u85cf\u6bcf\u4e00\u4e2a\u503c\u5f97\u56de\u5473\u7684\u7247\u6bb5</p>
        </div>
        <div class="action-group">
          <button class="btn-secondary" id="createLife">\u65b0\u589e\u8bb0\u5f55</button>
          <button class="btn-link" id="refreshLife">\u5237\u65b0</button>
        </div>
      </header>
      <div class="media-grid" id="lifeList">
        ${
          module.items.length
            ? module.items.map(renderLifeCard).join("")
            : `<div class="empty">\u8bb0\u5f55\u4e0b\u4e00\u4e2a\u6696\u5fc3\u77ac\u95f4\u5427</div>`
        }
      </div>
      ${renderPagination(module)}
    </section>
  `;

  container.querySelector("#createLife").addEventListener("click", () => openLifeForm());
  container.querySelector("#refreshLife").addEventListener("click", () => renderLifeMoments());

  const list = container.querySelector("#lifeList");
  list.addEventListener("click", (event) => {
    const actionBtn = event.target.closest("[data-action]");
    if (!actionBtn) return;
    const card = actionBtn.closest("[data-id]");
    if (!card) return;
    const record = module.items.find((item) => String(item.id) === card.dataset.id);
    if (!record) return;
    const action = actionBtn.dataset.action;
    if (action === "edit") {
      openLifeForm(record);
    } else if (action === "delete") {
      deleteLifeRecord(record);
    }
  });

  bindPaginationControls(container, module, renderLifeMoments);
}

async function renderDiaries() {
  const module = state.modules.diaries;
  await ensureModuleData("diaries", true);
  updateHeader();

  const rows = module.items.map(renderDiaryRow).join("");
  const container = getContentArea();
  container.innerHTML = `
    <section class="card">
      <header class="card-header">
        <div>
          <h2 class="card-title">\u751c\u871c\u65e5\u5fd7</h2>
          <p class="card-subtitle">\u628a\u5fc3\u60c5\u5199\u4e0b\u6765\uff0c\u8ba9\u7231\u610f\u5e38\u9a7b</p>
        </div>
        <div class="action-group">
          <button class="btn-secondary" id="createDiary">\u5199\u4e00\u7bc7\u65e5\u8bb0</button>
          <button class="btn-link" id="refreshDiaries">\u5237\u65b0</button>
        </div>
      </header>
      <div class="list-group" id="diaryList">
        ${rows || `<div class="empty">\u8fd8\u6ca1\u6709\u65e5\u8bb0\uff0c\u5199\u4e0b\u4eca\u5929\u7684\u5fc3\u60c5\u5427</div>`}
      </div>
      ${renderPagination(module)}
    </section>
  `;

  container.querySelector("#createDiary").addEventListener("click", () => openDiaryForm());
  container.querySelector("#refreshDiaries").addEventListener("click", () => renderDiaries());

  container.querySelector("#diaryList").addEventListener("click", (event) => {
    const actionBtn = event.target.closest("[data-action]");
    if (!actionBtn) return;
    const article = actionBtn.closest("[data-id]");
    if (!article) return;
    const diary = module.items.find((item) => String(item.id) === article.dataset.id);
    if (!diary) return;
    const action = actionBtn.dataset.action;
    if (action === "view") {
      openDiaryDetail(diary.id);
    } else if (action === "edit") {
      openDiaryForm(diary);
    } else if (action === "delete") {
      deleteDiary(diary);
    }
  });

  bindPaginationControls(container, module, renderDiaries);
}

async function renderReminders() {
  const module = state.modules.reminders;
  await ensureModuleData("reminders", true);
  updateHeader();

  const container = getContentArea();
  container.innerHTML = `
    <section class="card">
      <header class="card-header">
        <div>
          <h2 class="card-title">\u6e29\u6696\u63d0\u9192</h2>
          <p class="card-subtitle">\u522b\u5fd8\u4e86\u90a3\u4e9b\u503c\u5f97\u671f\u5f85\u7684\u5c0f\u4e8b</p>
        </div>
        <div class="action-group">
          <button class="btn-secondary" id="createReminder">\u65b0\u589e\u63d0\u9192</button>
          <button class="btn-link" id="refreshReminders">\u5237\u65b0</button>
        </div>
      </header>
      <div class="list-group" id="reminderList">
        ${
          module.items.length
            ? module.items.map(renderReminderCard).join("")
            : `<div class="empty">\u6682\u65e0\u63d0\u9192\uff0c\u5feb\u53bb\u6dfb\u52a0\u4e00\u4e2a\u5427</div>`
        }
      </div>
      ${renderPagination(module)}
    </section>
  `;

  container.querySelector("#createReminder").addEventListener("click", () => openReminderForm());
  container.querySelector("#refreshReminders").addEventListener("click", () => renderReminders());

  container.querySelector("#reminderList").addEventListener("click", (event) => {
    const actionBtn = event.target.closest("[data-action]");
    if (!actionBtn) return;
    const article = actionBtn.closest("[data-id]");
    if (!article) return;
    const reminder = module.items.find((item) => String(item.id) === article.dataset.id);
    if (!reminder) return;
    const action = actionBtn.dataset.action;
    if (action === "edit") {
      openReminderForm(reminder);
    } else if (action === "delete") {
      deleteReminder(reminder);
    } else if (action === "ack") {
      acknowledgeReminder(reminder);
    }
  });

  bindPaginationControls(container, module, renderReminders);
}

async function renderQuotes() {
  const module = state.modules.quotes;
  await ensureModuleData("quotes", true);
  updateHeader();

  const items = module.items.map(renderQuoteCard).join("");
  const container = getContentArea();
  container.innerHTML = `
    <section class="card">
      <header class="card-header">
        <div>
          <h2 class="card-title">\u6536\u85cf\u8bed\u5f55</h2>
          <p class="card-subtitle">\u628a\u5fc3\u52a8\u7684\u53e5\u5b50\u90fd\u6536\u597d</p>
        </div>
        <div class="action-group">
          <button class="btn-secondary" id="createQuote">\u65b0\u589e\u8bed\u5f55</button>
          <button class="btn-link" id="refreshQuotes">\u5237\u65b0</button>
        </div>
      </header>
      <div class="quote-grid" id="quoteList">
        ${items || `<div class="empty">\u6536\u85cf\u4e00\u53e5\u6e29\u67d4\u7684\u8bdd\u8bed\u5427</div>`}
      </div>
      ${renderPagination(module)}
    </section>
  `;

  container.querySelector("#createQuote").addEventListener("click", () => openQuoteForm());
  container.querySelector("#refreshQuotes").addEventListener("click", () => renderQuotes());

  container.querySelector("#quoteList").addEventListener("click", (event) => {
    const actionBtn = event.target.closest("[data-action]");
    if (!actionBtn) return;
    const block = actionBtn.closest("[data-id]");
    if (!block) return;
    const quote = module.items.find((item) => String(item.id) === block.dataset.id);
    if (!quote) return;
    const action = actionBtn.dataset.action;
    if (action === "edit") {
      openQuoteForm(quote);
    } else if (action === "delete") {
      deleteQuote(quote);
    }
  });

  bindPaginationControls(container, module, renderQuotes);
}

async function renderNotes() {
  const module = state.modules.notes;
  await ensureModuleData("notes", true);
  updateHeader();

  const items = module.items.map(renderNoteCard).join("");
  const container = getContentArea();
  container.innerHTML = `
    <section class="card">
      <header class="card-header">
        <div>
          <h2 class="card-title">\u5f69\u8272\u6807\u7b7e</h2>
          <p class="card-subtitle">\u968f\u624b\u8bb0\u5f55\u7075\u611f\u548c\u5631\u6258</p>
        </div>
        <div class="action-group">
          <button class="btn-secondary" id="createNote">\u65b0\u589e\u4fbf\u7b7e</button>
          <button class="btn-link" id="refreshNotes">\u5237\u65b0</button>
        </div>
      </header>
      <div class="note-grid" id="noteList">
        ${items || `<div class="empty">\u5199\u4e0b\u4e00\u53e5\u60f3\u8bf4\u7684\u8bdd\u5427</div>`}
      </div>
      ${renderPagination(module)}
    </section>
  `;

  container.querySelector("#createNote").addEventListener("click", () => openNoteForm());
  container.querySelector("#refreshNotes").addEventListener("click", () => renderNotes());

  const noteList = container.querySelector("#noteList");
  noteList.addEventListener("click", (event) => {
    const actionBtn = event.target.closest("button[data-action]");
    if (!actionBtn) {
      return;
    }
    event.preventDefault();
    event.stopPropagation();
    const card = actionBtn.closest("[data-id]");
    if (!card) {
      return;
    }
    const noteId = card.dataset.id;
    const note = module.items.find((item) => String(item.id) === noteId);
    if (!note) {
      notify("\u672a\u627e\u5230\u8be5\u4fbf\u7b7e\uff0c\u8bf7\u5237\u65b0\u540e\u518d\u8bd5", "error");
      return;
    }
    const action = actionBtn.dataset.action;
    if (action === "edit") {
      openNoteForm(note);
    } else if (action === "delete") {
      deleteNote(note);
    }
  });

  bindPaginationControls(container, module, renderNotes);
}

async function renderAlbum() {
  const module = state.modules.album;
  await ensureModuleData("album", true);
  updateHeader();

  const items = module.items.map(renderAlbumCard).join("");
  const container = getContentArea();
  container.innerHTML = `
    <section class="card">
      <header class="card-header">
        <div>
          <h2 class="card-title">\u7231\u7684\u76f8\u518c</h2>
          <p class="card-subtitle">\u628a\u56de\u5fc6\u653e\u8fdb\u4f1a\u53d1\u5149\u7684\u76f8\u518c</p>
        </div>
        <div class="action-group">
          <button class="btn-secondary" id="createAlbum">\u65b0\u589e\u7167\u7247/\u89c6\u9891</button>
          <button class="btn-link" id="refreshAlbum">\u5237\u65b0</button>
        </div>
      </header>
      <div class="media-grid" id="albumList">
        ${items || `<div class="empty">\u4e0a\u4f20\u4e00\u5f20\u65b0\u7684\u56de\u5fc6\u5427</div>`}
      </div>
      ${renderPagination(module)}
    </section>
  `;

  container.querySelector("#createAlbum").addEventListener("click", () => openAlbumForm());
  container.querySelector("#refreshAlbum").addEventListener("click", () => renderAlbum());

  container.querySelector("#albumList").addEventListener("click", (event) => {
    const actionBtn = event.target.closest("[data-action]");
    if (!actionBtn) return;
    const card = actionBtn.closest("[data-id]");
    if (!card) return;
    const media = module.items.find((item) => String(item.id) === card.dataset.id);
    if (!media) return;
    if (actionBtn.dataset.action === "delete") {
      deleteAlbumMedia(media);
    } else if (actionBtn.dataset.action === "edit") {
      openAlbumForm(media);
    }
  });

  bindPaginationControls(container, module, renderAlbum);
}

async function renderAnalytics() {
  updateHeader();
  const container = getContentArea();
  container.innerHTML = `
    <section class="card">
      <header class="card-header">
        <div>
          <h2 class="card-title">\u751c\u871c\u7edf\u8ba1\u56fe</h2>
          <p class="card-subtitle">\u6b63\u5728\u4ece\u7cfb\u7edf\u52a0\u8f7d\u5b9e\u65f6\u6570\u636e...</p>
        </div>
      </header>
      <div class="empty">\u6b63\u5728\u4ece\u6570\u636e\u5e93\u4e2d\u6574\u7406\u60a8\u4eec\u7684\u751c\u871c\u8bb0\u5f55...</div>
    </section>
  `;
  try {
    const overview = await apiRequest("/api/analytics/overview");
    const goal = overview?.goalSummary || {};
    const mood = overview?.moodSummary || {};
    const interactions = Array.isArray(overview?.interactions) ? overview.interactions : [];
    const suggestions = Array.isArray(overview?.suggestions) ? overview.suggestions : [];

    const goalRate = clampPercentage(goal.completionRate);
    const moodRate = clampPercentage(mood.dominantPercentage);
    const moodLabel = escapeHtml(mood.dominantMood || "\u6682\u65e0\u8bb0\u5f55");
    const moodCount = mood.totalEntries || 0;
    const breakdownHtml = (mood.breakdown || [])
      .map((slice) => `<li>${escapeHtml(slice.label || "\u672a\u586b\u5199")} &middot; ${slice.count || 0} \u6b21</li>`)
      .join("");
    const interactionHtml = interactions.length
      ? interactions.map((item) => `<li>${escapeHtml(item.label || "")} &middot; ${item.count || 0} \u6b21</li>`).join("")
      : "<li>\u6682\u65e0\u4eba\u6d3b\u8bb0\u5f55</li>";
    const suggestionHtml = suggestions.length
      ? suggestions.map((tip) => `<li class="timeline-item">${escapeHtml(tip)}</li>`).join("")
      : `<li class="timeline-item">\u7ee7\u7eed\u4fdd\u6301\u7231\u7684\u70ed\u5ea6\uff0c\u6bcf\u4e00\u5929\u90fd\u662f\u65b0\u7684\u751c\u871c\u8d77\u70b9\u3002</li>`;

    container.innerHTML = `
      <section class="card">
        <header class="card-header">
          <div>
            <h2 class="card-title">\u751c\u871c\u7edf\u8ba1\u56fe</h2>
            <p class="card-subtitle">\u6570\u636e\u8ba9\u4f60\u4eec\u66f4\u61c2\u751c\u871c\u5c0f\u65f6\u5149</p>
          </div>
        </header>
        <div class="analytics-grid">
          <div class="analytics-card">
            <h3>\u5fc3\u60c5\u5206\u5e03</h3>
            <div class="chart-bar"><span style="width:${moodCount === 0 ? 0 : moodRate}%"></span></div>
            <p>${moodCount === 0 ? "\u6682\u65e0\u5fc3\u60c5\u8bb0\u5f55" : `${moodRate}% \u7684\u65f6\u523b\u662f\u201c${moodLabel}\u201d`}</p>
            ${breakdownHtml ? `<ul class="analytics-list analytics-list--compact">${breakdownHtml}</ul>` : ""}
          </div>
          <div class="analytics-card">
            <h3>\u76ee\u6807\u5b8c\u6210\u7387</h3>
            <div class="chart-ring" data-percent="${goalRate}">
              <svg viewBox="0 0 36 36">
                <path class="bg" d="M18 2.0845a 15.9155 15.9155 0 0 1 0 31.831"></path>
                <path class="progress" stroke-dasharray="${goalRate},100" d="M18 2.0845a 15.9155 15.9155 0 0 1 0 31.831"></path>
              </svg>
              <strong>${goalRate}%</strong>
            </div>
            <p>\u5df2\u5b8c\u6210 ${goal.completed || 0} / ${goal.total || 0} \u4e2a\u5c0f\u76ee\u6807</p>
            <div class="analytics-tags">
              <span>\u8fdb\u884c\u4e2d ${goal.inProgress || 0}</span>
              <span>\u5f85\u542f\u52a8 ${goal.pending || 0}</span>
              <span>\u5df2\u5f52\u6863 ${goal.archived || 0}</span>
            </div>
          </div>
          <div class="analytics-card">
            <h3>\u4e92\u52a8\u70b9\u6ef4</h3>
            <ul class="analytics-list">
              ${interactionHtml}
            </ul>
          </div>
        </div>
      </section>
      <section class="card muted">
        <header class="card-header">
          <div>
            <h2 class="card-title">\u751c\u871c\u6559\u7a0b\u63d0\u9192</h2>
            <p class="card-subtitle">\u6839\u636e\u5b9e\u65f6\u6570\u636e\u7ed9\u51fa\u7684\u6e29\u6696\u5929\u5730</p>
          </div>
        </header>
        <ul class="timeline">
          ${suggestionHtml}
        </ul>
      </section>
    `;
  } catch (error) {
    console.error(error);
    notify(error.message || "\u7edf\u8ba1\u6570\u636e\u52a0\u8f7d\u5931\u8d25", "error");
    container.innerHTML = `
      <section class="card">
        <header class="card-header">
          <div>
            <h2 class="card-title">\u751c\u871c\u7edf\u8ba1\u56fe</h2>
          </div>
        </header>
        <div class="empty">\u7edf\u8ba1\u6570\u636e\u52a0\u8f7d\u5931\u8d25\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u5373\u53ef\u91cd\u8bd5\u3002</div>
      </section>
    `;
  }
}

async function renderUserAdmin() {
  updateHeader();
  const container = getContentArea();
  container.innerHTML = `
    <section class="card">
      <header class="card-header">
        <div>
          <h2 class="card-title">\u7528\u6237\u7ba1\u7406</h2>
          <p class="card-subtitle">\u5f85\u4f60\u7ef4\u62a4\u4e24\u4f4d\u7231\u4eba\u7684\u751f\u6d3b\u5c0f\u6863\u6848</p>
        </div>
        <button class="btn-secondary" id="inviteUserBtn">\u9080\u8bf7\u529f\u80fd\u7b49\u4f60\u5f00\u542f</button>
      </header>
      <div class="table-scroll">
        <table class="data-table data-table--wide">
          <thead>
            <tr>
              <th>\u8d26\u53f7</th>
              <th>\u6635\u79f0</th>
              <th>\u7231\u4eba\u79f0\u547c</th>
              <th>\u90ae\u7bb1</th>
              <th>\u8054\u7cfb\u65b9\u5f0f</th>
              <th>\u6ce8\u518c\u65f6\u95f4</th>
              <th>\u6700\u8fd1\u767b\u5f55</th>
              <th>\u76ee\u6807\u5b8c\u6210</th>
              <th>\u751c\u871c\u65e5\u5fd7</th>
              <th>\u751f\u6d3b\u77ac\u95f4</th>
              <th>\u6e29\u6696\u63d0\u9192</th>
              <th>\u5f69\u8272\u6807\u7b7e</th>
              <th>\u7231\u7684\u76f8\u518c</th>
              <th>\u4e3b\u9898\u914d\u8272</th>
            </tr>
          </thead>
          <tbody id="userAdminBody">
            <tr><td colspan="14" class="muted">\u6570\u636e\u52a0\u8f7d\u4e2d...</td></tr>
          </tbody>
        </table>
      </div>
    </section>
  `;

  const inviteBtn = container.querySelector("#inviteUserBtn");
  if (inviteBtn) {
    inviteBtn.addEventListener("click", () => notify("\u9080\u8bf7\u529f\u80fd\u5373\u5c06\u4e0a\u7ebf\uff0c\u8bf7\u7b49\u5f85~"));
  }

  try {
    const data = await apiRequest("/api/admin/users");
    const list = Array.isArray(data) ? data : [];
    const tbody = container.querySelector("#userAdminBody");
    if (!tbody) return;
    if (!list.length) {
      tbody.innerHTML = `<tr><td colspan="14" class="muted">\u6682\u65e0\u7528\u6237\u8bb0\u5f55</td></tr>`;
      return;
    }
    tbody.innerHTML = list
      .map((user) => {
        const created = user.createdAt ? formatDateTime(user.createdAt) : "--";
        const lastLogin = user.lastLoginAt ? formatDateTime(user.lastLoginAt) : "\u5c1a\u672a\u767b\u5f55";
        const partner = user.partnerName ? escapeHtml(user.partnerName) : "\u5c1a\u672a\u586b\u5199";
        const contact = user.contactPhone ? escapeHtml(user.contactPhone) : "\u5c1a\u672a\u586b\u5199";
        const goalSummary = `${user.goalCompleted || 0} / ${user.goalTotal || 0}`;
        return `
          <tr>
            <td>${escapeHtml(user.username || "")}</td>
            <td>${escapeHtml(user.displayName || "")}</td>
            <td>${partner}</td>
            <td>${escapeHtml(user.email || "--")}</td>
            <td>${contact}</td>
            <td>${created}</td>
            <td>${lastLogin}</td>
            <td>${goalSummary}</td>
            <td>${user.diaryCount || 0}</td>
            <td>${user.lifeRecordCount || 0}</td>
            <td>${user.reminderCount || 0}</td>
            <td>${user.noteCount || 0}</td>
            <td>${user.albumCount || 0}</td>
            <td>${escapeHtml(describeTheme(user.themePreference))}</td>
          </tr>
        `;
      })
      .join("");
  } catch (error) {
    console.error(error);
    notify(error.message || "\u7528\u6237\u6570\u636e\u52a0\u8f7d\u5931\u8d25", "error");
    const tbody = container.querySelector("#userAdminBody");
    if (tbody) {
      tbody.innerHTML = `<tr><td colspan="14" class="muted">\u7528\u6237\u6570\u636e\u52a0\u8f7d\u5931\u8d25\uff0c\u8bf7\u5237\u65b0\u91cd\u8bd5</td></tr>`;
    }
  }
}

function renderGoalCard(item) {
  const cover = item.coverPath
    ? `<div class="list-cover" data-preview="${escapeHtml(resolveMediaUrl(item.coverPath))}" data-media-type="image" data-preview-caption="${escapeHtml(item.title || "\u5c0f\u76ee\u6807")}"><img src="${escapeHtml(resolveMediaUrl(item.coverPath))}" alt="${escapeHtml(item.title || "\u5c0f\u76ee\u6807")}"></div>`
    : `<div class="list-cover placeholder">\u6682\u65e0\u5c01\u9762</div>`;
  return `
    <article class="list-item" data-id="${item.id}">
      ${cover}
      <header class="list-item-head">
        <h3>${escapeHtml(item.title || "\u672a\u547d\u540d\u76ee\u6807")}</h3>
        <span class="badge">${mapGoalStatus(item.status)}</span>
      </header>
      <p>${escapeHtml(item.description || "\u6682\u65e0\u63cf\u8ff0")}</p>
      <footer class="list-item-foot">
        <div class="muted">
          <span>${item.targetDate ? `\u8ba1\u5212\u65e5\u671f\uff1a${formatDate(item.targetDate)}` : "\u672a\u8bbe\u7f6e\u65e5\u671f"}</span>
          <span>\u8fdb\u5ea6\uff1a${item.progressRatio ?? 0}%</span>
        </div>
        <div class="list-item-actions">
          <button type="button" class="btn-text" data-action="edit">\u7f16\u8f91</button>
          <button type="button" class="btn-text danger" data-action="delete">\u5220\u9664</button>
        </div>
      </footer>
    </article>
  `;
}

function renderRoutineItem(item) {
  const thumb = item.backgroundPath
    ? `<div class="timeline-thumb" data-preview="${escapeHtml(resolveMediaUrl(item.backgroundPath))}" data-media-type="image" data-preview-caption="${escapeHtml(item.label || "\u6bcf\u65e5\u65e5\u7a0b")}"><img src="${escapeHtml(resolveMediaUrl(item.backgroundPath))}" alt="${escapeHtml(item.label || "\u6bcf\u65e5\u65e5\u7a0b")}"></div>`
    : `<div class="timeline-thumb placeholder">\u6682\u65e0\u914d\u56fe</div>`;
  return `
    <div class="timeline-item" data-id="${item.id}">
      <div class="timeline-head">
        ${thumb}
        <div class="timeline-info">
          <div class="timeline-time"><strong>${escapeHtml(item.timeRange || "")}</strong> ${escapeHtml(item.label || "")}</div>
          <p>${escapeHtml(item.description || "\u6682\u672a\u586b\u5199\u5185\u5bb9")}</p>
          ${item.emoji ? `<div class="tag">${escapeHtml(item.emoji)}</div>` : ""}
        </div>
        <div class="timeline-actions">
          <button type="button" class="btn-text" data-action="edit">\u7f16\u8f91</button>
          <button type="button" class="btn-text danger" data-action="delete">\u5220\u9664</button>
        </div>
      </div>
    </div>
  `;
}

function renderLifeCard(item) {
  const cover = item.coverPath
    ? `<div class="media-display image" data-preview="${escapeHtml(resolveMediaUrl(item.coverPath))}" data-media-type="image"><img src="${escapeHtml(resolveMediaUrl(item.coverPath))}" alt="${escapeHtml(item.title || "\u751f\u6d3b\u77ac\u95f4")}"></div>`
    : `<div class="media-placeholder">\u6682\u65e0\u5c01\u9762</div>`;
  return `
    <div class="media-card" data-id="${item.id}">
      ${cover}
      <div class="media-overlay">
        <h3>${escapeHtml(item.title || "\u672a\u547d\u540d\u77ac\u95f4")}</h3>
        <p>${escapeHtml(item.description || "\u8bb0\u5f55\u4f60\u4eec\u7684\u6545\u4e8b")}</p>
        <small>${item.recordDate ? formatDate(item.recordDate) : ""}</small>
        <div class="media-actions">
          <button type="button" class="btn-text" data-action="edit">\u7f16\u8f91</button>
          <button type="button" class="btn-text danger" data-action="delete">\u5220\u9664</button>
        </div>
      </div>
    </div>
  `;
}

function renderDiaryRow(item) {
  const content = item.content || "";
  const preview = content.length > 120 ? `${content.slice(0, 120)}\u2026` : content;
  const cover = item.coverPath
    ? `<div class="list-cover" data-preview="${escapeHtml(resolveMediaUrl(item.coverPath))}" data-media-type="image" data-preview-caption="${escapeHtml(item.title || "\u751c\u871c\u65e5\u5fd7")}"><img src="${escapeHtml(resolveMediaUrl(item.coverPath))}" alt="${escapeHtml(item.title || "\u751c\u871c\u65e5\u5fd7")}"></div>`
    : `<div class="list-cover placeholder">\u6682\u65e0\u5c01\u9762</div>`;
  return `
    <article class="list-item" data-id="${item.id}">
      ${cover}
      <header class="list-item-head">
        <span class="badge">${formatDate(item.entryDate)}</span>
        <h3>${escapeHtml(item.title || "\u672a\u547d\u540d\u65e5\u8bb0")}</h3>
        ${item.mood ? `<span class="badge badge-soft">${escapeHtml(item.mood)}</span>` : ""}
      </header>
      <p>${escapeHtml(preview || "\u70b9\u51fb\u67e5\u770b\u8be6\u7ec6\u5185\u5bb9")}</p>
      <footer class="list-item-foot">
        <span class="muted">${item.weather ? `\u5929\u6c14\uff1a${escapeHtml(item.weather)}` : ""}</span>
        <div class="list-item-actions">
          <button type="button" class="btn-text" data-action="view">\u67e5\u770b</button>
          <button type="button" class="btn-text" data-action="edit">\u7f16\u8f91</button>
          <button type="button" class="btn-text danger" data-action="delete">\u5220\u9664</button>
        </div>
      </footer>
    </article>
  `;
}

function renderReminderCard(item) {
  const cover = item.coverPath
    ? `<div class="list-cover" data-preview="${escapeHtml(resolveMediaUrl(item.coverPath))}" data-media-type="image" data-preview-caption="${escapeHtml(item.title || "\u6e29\u6696\u63d0\u9192")}"><img src="${escapeHtml(resolveMediaUrl(item.coverPath))}" alt="${escapeHtml(item.title || "\u6e29\u6696\u63d0\u9192")}"></div>`
    : `<div class="list-cover placeholder">\u6682\u65e0\u914d\u56fe</div>`;
  return `
    <article class="list-item" data-id="${item.id}">
      ${cover}
      <header class="list-item-head">
        <h3>${escapeHtml(item.title || "\u672a\u547d\u540d\u63d0\u9192")}</h3>
        <span class="badge badge-${item.priority || "MEDIUM"}">${mapReminderPriority(item.priority)}</span>
      </header>
      <p>${escapeHtml(item.message || "\u6ca1\u6709\u8be6\u7ec6\u5185\u5bb9")}</p>
      <footer class="list-item-foot">
      <div class="muted">
        <span>${item.remindAt ? `\u63d0\u9192\u65f6\u95f4\uff1a${formatDateTime(item.remindAt)}` : "\u672a\u8bbe\u7f6e\u65f6\u95f4"}</span>
        <span>${item.acknowledged ? "\u5df2\u786e\u8ba4" : "\u5f85\u786e\u8ba4"}</span>
      </div>
      <div class="list-item-actions">
        <button type="button" class="btn-text" data-action="ack">${item.acknowledged ? "\u53d6\u6d88\u786e\u8ba4" : "\u6807\u8bb0\u5b8c\u6210"}</button>
        <button type="button" class="btn-text" data-action="edit">\u7f16\u8f91</button>
        <button type="button" class="btn-text danger" data-action="delete">\u5220\u9664</button>
      </div>
    </footer>
  </article>
`;
}

function renderQuoteCard(item) {
  const cover = item.coverPath
    ? `<div class="quote-cover" data-preview="${escapeHtml(resolveMediaUrl(item.coverPath))}" data-media-type="image" data-preview-caption="${escapeHtml(item.content || "")}"><img src="${escapeHtml(resolveMediaUrl(item.coverPath))}" alt="\u8bed\u5f55\u914d\u56fe"></div>`
    : `<div class="quote-cover"><div class="list-cover placeholder">\u6682\u65e0\u914d\u56fe</div></div>`;
  return `
    <blockquote class="quote-card" data-id="${item.id}">
      ${cover}
      <p>${escapeHtml(item.content || "")}</p>
      <footer>${escapeHtml(item.source || "\u533f\u540d")} \u00b7 ${escapeHtml(item.moodTag || "\u5fc3\u60c5\u672a\u8bbe\u5b9a")}</footer>
      <div class="quote-actions">
        <button type="button" class="btn-text" data-action="edit">\u7f16\u8f91</button>
        <button type="button" class="btn-text danger" data-action="delete">\u5220\u9664</button>
      </div>
    </blockquote>
  `;
}

function renderNoteCard(item) {
  const cover = item.coverPath
    ? `<div class="note-cover" data-preview="${escapeHtml(resolveMediaUrl(item.coverPath))}" data-media-type="image" data-preview-caption="${escapeHtml(item.title || "\u5f69\u8272\u6807\u7b7e")}"><img src="${escapeHtml(resolveMediaUrl(item.coverPath))}" alt="${escapeHtml(item.title || "\u5f69\u8272\u6807\u7b7e")}"></div>`
    : "";
  return `
    <div class="note-card" data-id="${item.id}" style="background:${escapeHtml(item.colorHex || "#ffd6e8")};">
      ${cover}
      <header>
        <h3>${escapeHtml(item.title || "\u672a\u547d\u540d\u6807\u7b7e")}</h3>
        ${item.pinned ? `<span class="badge badge-warning">\u7f6e\u9876</span>` : ""}
      </header>
      <p>${escapeHtml(item.content || "")}</p>
      <div class="note-footer">
        <small>${item.expiresAt ? `\u8fc7\u671f\u65f6\u95f4\uff1a${formatDateTime(item.expiresAt)}` : "\u957f\u671f\u6709\u6548"}</small>
        <div class="note-actions">
          <button type="button" class="btn-text" data-action="edit">\u7f16\u8f91</button>
          <button type="button" class="btn-text danger" data-action="delete">\u5220\u9664</button>
        </div>
      </div>
    </div>
  `;
}

function renderAlbumCard(item) {
  const isVideo = String(item.mediaType || "").toUpperCase().includes("VIDEO");
  const previewAttr = `data-preview="${escapeHtml(resolveMediaUrl(item.filePath))}" data-media-type="${isVideo ? "video" : "image"}" data-preview-caption="${escapeHtml(item.title || "\u56de\u5fc6")}"`;
  const media = isVideo
    ? `<div class="media-display video" ${previewAttr}><video muted preload="metadata" playsinline src="${escapeHtml(resolveMediaUrl(item.filePath))}"></video><span class="media-display__icon">\u25b6</span></div>`
    : `<div class="media-display image" ${previewAttr}><img src="${escapeHtml(resolveMediaUrl(item.filePath))}" alt="${escapeHtml(item.title || "\u56de\u5fc6")}"></div>`;
  return `
    <div class="media-card" data-id="${item.id}">
      ${media}
      <div class="media-overlay">
        <h3>${escapeHtml(item.title || "\u65e0\u6807\u9898")}</h3>
        <p>${escapeHtml(item.description || "")}</p>
        <small>${item.takenAt ? formatDateTime(item.takenAt) : ""}</small>
        <div class="media-actions">
          <button type="button" class="btn-text" data-action="edit">\u7f16\u8f91</button>
          <button type="button" class="btn-text danger" data-action="delete">\u5220\u9664</button>
        </div>
      </div>
    </div>
  `;
}

function renderPagination(module) {
  if (!module.total || module.total <= module.size) {
    return "";
  }
  const totalPages = Math.ceil(module.total / module.size);
  return `
    <div class="pagination">
      <button class="btn-link" data-page="prev" ${module.page <= 1 ? "disabled" : ""}>\u4e0a\u4e00\u9875</button>
      <span>\u7b2c ${module.page} / ${totalPages} \u9875</span>
      <button class="btn-link" data-page="next" ${module.page >= totalPages ? "disabled" : ""}>\u4e0b\u4e00\u9875</button>
    </div>
  `;
}

function bindPaginationControls(container, module, reloadFn) {
  container.querySelectorAll(".pagination").forEach((pager) => {
    pager.addEventListener("click", (event) => {
      const button = event.target.closest("button[data-page]");
      if (!button || button.disabled) return;
      const totalPages = Math.ceil(module.total / module.size);
      if (button.dataset.page === "prev" && module.page > 1) {
        module.page -= 1;
        reloadFn();
      } else if (button.dataset.page === "next" && module.page < totalPages) {
        module.page += 1;
        reloadFn();
      }
    });
  });
}

function getContentArea() {
  return document.getElementById("contentArea");
}

function mapGoalStatus(status) {
  const found = GOAL_STATUS_OPTIONS.find((item) => item.value === status);
  return found ? found.label : "\u672a\u5f00\u59cb";
}

function mapReminderPriority(priority) {
  const found = REMINDER_PRIORITY_OPTIONS.find((item) => item.value === priority);
  return found ? found.label : "\u666e\u901a\u63d0\u9192";
}

async function refreshModule(key) {
  const module = state.modules[key];
  if (!module) return;
  module.lastLoadedAt = 0;
  await ensureModuleData(key, true);
}

async function uploadFile(file) {
  if (!file) return null;
  const formData = new FormData();
  formData.append("file", file);
  return apiRequest("/api/files", { method: "POST", body: formData });
}

function formatDate(value) {
  if (!value) return "--";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return String(value).slice(0, 10);
  return new Intl.DateTimeFormat("zh-CN", {
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  }).format(date);
}

function formatDateTime(value) {
  if (!value) return "--";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) {
    return String(value).replace("T", " ").slice(0, 16);
  }
  const formatter = new Intl.DateTimeFormat("zh-CN", {
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
  });
  return formatter.format(date);
}

function toDateInputValue(value) {
  if (!value) return "";
  if (value instanceof Date) {
    return value.toISOString().slice(0, 10);
  }
  if (typeof value === "string" && value.length >= 10) {
    return value.slice(0, 10);
  }
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return "";
  return date.toISOString().slice(0, 10);
}

function toDateTimeInputValue(value) {
  if (!value) return "";
  if (typeof value === "string") {
    return value.replace(" ", "T").slice(0, 16);
  }
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return "";
  const iso = new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString();
  return iso.slice(0, 16);
}

function normalizeDateTimeValue(value) {
  if (!value) return null;
  if (value.length === 16) return `${value}:00`;
  if (value.length === 19) return value;
  return value;
}

function clampPercentage(value) {
  const num = Number.isFinite(value) ? Number(value) : parseFloat(value);
  if (Number.isNaN(num)) return 0;
  return Math.min(100, Math.max(0, Math.round(num)));
}

function describeTheme(theme) {
  switch ((theme || "").toLowerCase()) {
    case "pink":
      return "\u6d6a\u6f2b\u7c89";
    case "lavender":
      return "\u7f57\u5170\u53ef\u53ef";
    case "midnight":
      return "\u6697\u591c\u6df1\u84dd";
    default:
      return "\u81ea\u52a8";
  }
}

function escapeHtml(text = "") {
  return String(text)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

function resolveMediaUrl(path) {
  if (!path) return "";
  if (/^https?:/i.test(path)) return path;
  if (path.startsWith("/")) return path;
  return `${MEDIA_BASE}/${path}`.replace(/\/{2,}/g, "/");
}

function notify(message, type = "info") {
  toast.push(message, type);
}

function openConfirmDialog({
  title,
  message,
  note,
  confirmText = "\u786e\u8ba4",
  cancelText = "\u53d6\u6d88",
  danger = true,
}) {
  return new Promise((resolve) => {
    let settled = false;
    const finish = (result) => {
      if (!settled) {
        settled = true;
        resolve(result);
      }
    };
    const box = document.createElement("div");
    box.className = "confirm-dialog";
    box.innerHTML = `
      <p class="confirm-message">${escapeHtml(message || "")}</p>
      ${note ? `<p class="confirm-note">${escapeHtml(note)}</p>` : ""}
    `;
    modal.open({
      title,
      content: box,
      submitText: confirmText,
      cancelText,
      submitClassName: danger ? "btn-danger" : "",
      onSubmit: () => {
        finish(true);
        return true;
      },
      onCancel: () => finish(false),
    });
  });
}
// \u5c0f\u76ee\u6807\u6e05\u5355\u8868\u5355\u5f39\u7a97\uff1a\u7528\u4e8e\u65b0\u589e\u4e0e\u7f16\u8f91\u76ee\u6807
function openProfileEditor() {
  const detail = state.profile.detail || {};
  const form = document.createElement("form");
  form.className = "form-grid";
  const avatarUrl = state.profile.avatarPath ? escapeHtml(resolveMediaUrl(state.profile.avatarPath)) : "";
  form.innerHTML = `
    <div class="form-field">
      <label>\u5c55\u793a\u6635\u79f0 <span class="required">*</span></label>
      <input name="displayName" value="${escapeHtml(state.profile.displayName || "")}" required>
    </div>
    <div class="form-field">
      <label>\u8054\u7cfb\u90ae\u7bb1</label>
      <input name="email" type="email" value="${escapeHtml(state.profile.email || "")}" placeholder="\u53ef\u9009\uff1a\u7528\u4e8e\u7cfb\u7edf\u901a\u77e5">
    </div>
    <div class="form-field full">
      <label>\u5934\u50cf</label>
      <div class="avatar-upload">
        <div class="avatar-upload__preview" ${avatarUrl ? `data-preview="${avatarUrl}" data-media-type="image"` : ""}>
          ${avatarUrl ? `<img src="${avatarUrl}" alt="\u5934\u50cf\u9884\u89c8">` : `<span class="muted">\u6682\u672a\u8bbe\u7f6e\u5934\u50cf</span>`}
        </div>
        <input type="file" name="avatar" accept="image/*">
      </div>
    </div>
    <div class="form-field full">
      <label>\u4e2a\u4eba\u7b80\u4ecb <span class="required">*</span></label>
      <textarea name="selfIntroduction" rows="3" required placeholder="\u5199\u4e00\u70b9\u60f3\u5bf9\u81ea\u5df1\u548c\u7231\u4eba\u8bf4\u7684\u8bdd">${escapeHtml(detail?.selfIntroduction || "\u4e00\u8d77\u597d\u597d\u751f\u6d3b\uff0c\u628a\u6545\u4e8b\u5199\u6210\u95ea\u95ea\u53d1\u5149\u7684\u7bc7\u7ae0")}</textarea>
    </div>
    <div class="form-field">
      <label>\u5de5\u4f5c\u5c97\u4f4d</label>
      <input name="jobTitle" value="${escapeHtml(detail?.jobTitle || "")}">
    </div>
    <div class="form-field">
      <label>\u5de5\u4f5c\u5730\u70b9</label>
      <input name="workplace" value="${escapeHtml(detail?.workplace || "")}">
    </div>
    <div class="form-field">
      <label>\u5174\u8da3\u7231\u597d</label>
      <input name="hobbies" value="${escapeHtml(detail?.hobbies || "")}">
    </div>
    <div class="form-field">
      <label>\u7231\u4eba\u79f0\u547c <span class="required">*</span></label>
      <input name="partnerName" value="${escapeHtml(detail?.partnerName || "\u7231\u4eba")}" required>
    </div>
    <div class="form-field">
      <label>\u7231\u4eba\u7279\u8d28</label>
      <input name="partnerTraits" value="${escapeHtml(detail?.partnerTraits || "")}">
    </div>
    <div class="form-field full">
      <label>\u604b\u7231\u6545\u4e8b</label>
      <textarea name="loveStory" rows="3" placeholder="\u8bb0\u5f55\u4f60\u4eec\u7684\u751c\u871c\u5fc3\u4e8b">${escapeHtml(detail?.loveStory || "")}</textarea>
    </div>
    <div class="form-field">
      <label>\u8054\u7cfb\u5fae\u4fe1</label>
      <input name="contactWechat" value="${escapeHtml(detail?.contactWechat || "")}">
    </div>
    <div class="form-field">
      <label>\u8054\u7cfb\u7535\u8bdd</label>
      <input name="contactPhone" value="${escapeHtml(detail?.contactPhone || "")}">
    </div>
  `;

  const avatarInput = form.querySelector('input[name="avatar"]');
  const avatarPreview = form.querySelector('.avatar-upload__preview');
  avatarInput.addEventListener('change', () => {
    avatarPreview.innerHTML = '';
    avatarPreview.removeAttribute('data-preview');
    avatarPreview.removeAttribute('data-media-type');
    const file = avatarInput.files[0];
    if (!file) {
      if (avatarUrl) {
        avatarPreview.setAttribute('data-preview', avatarUrl);
        avatarPreview.setAttribute('data-media-type', 'image');
        avatarPreview.innerHTML = `<img src="${avatarUrl}" alt="\u5934\u50cf\u9884\u89c8">`;
      } else {
        avatarPreview.innerHTML = '<span class="muted">\u6682\u672a\u8bbe\u7f6e\u5934\u50cf</span>';
      }
      return;
    }
    const url = URL.createObjectURL(file);
    avatarPreview.setAttribute('data-preview', url);
    avatarPreview.setAttribute('data-media-type', 'image');
    const img = document.createElement('img');
    img.src = url;
    img.alt = '\u5934\u50cf\u9884\u89c8';
    avatarPreview.appendChild(img);
  });

  modal.open({
    title: "\u7f16\u8f91\u4e2a\u4eba\u6863\u6848",
    content: form,
    submitText: "\u4fdd\u5b58",
    onSubmit: async () => {
      if (!form.reportValidity()) {
        return false;
      }
      const payload = {
        displayName: form.displayName.value.trim(),
        email: form.email.value.trim() || null,
        selfIntroduction: form.selfIntroduction.value.trim(),
        jobTitle: form.jobTitle.value.trim() || null,
        workplace: form.workplace.value.trim() || null,
        hobbies: form.hobbies.value.trim() || null,
        partnerName: form.partnerName.value.trim(),
        partnerTraits: form.partnerTraits.value.trim() || null,
        loveStory: form.loveStory.value.trim() || null,
        contactWechat: form.contactWechat.value.trim() || null,
        contactPhone: form.contactPhone.value.trim() || null,
        avatarPath: state.profile.avatarPath || null,
      };

      if (!payload.selfIntroduction || !payload.partnerName) {
        notify("\u8bf7\u5b8c\u5584\u5fc5\u586b\u4fe1\u606f", "error");
        return false;
      }

      const avatarFile = avatarInput.files[0];
      if (avatarFile) {
        const upload = await uploadFile(avatarFile);
        payload.avatarPath = upload.filePath;
      }

      const updated = await apiRequest("/api/profile", { method: "PUT", body: payload });
      state.profile.displayName = updated.displayName || state.profile.displayName;
      state.profile.avatarPath = updated.avatarPath || state.profile.avatarPath;
      state.profile.email = updated.email || state.profile.email || "";
      state.profile.themePreference = updated.themePreference || state.profile.themePreference;
      state.profile.detail = updated.profile || state.profile.detail;
      state.profile.partnerTagline = updated.profile?.partnerName
        ? `\u548c ${updated.profile.partnerName} \u7684\u751c\u871c\u65e5\u5e38`
        : state.profile.partnerTagline;
      localStorage.setItem(PROFILE_CACHE_KEY, JSON.stringify(updated));
      updateSidebar();
      updateHeader();
      applyTheme(state.profile.themePreference);
      notify("\u4e2a\u4eba\u6863\u6848\u5df2\u66f4\u65b0", "success");
      return true;
    },
  });
}

function openGoalForm(goal) {
  const form = document.createElement("form");
  form.className = "form-grid";
  form.innerHTML = `
    <div class="form-field full">
      <label>\u76ee\u6807\u6807\u9898 <span class="required">*</span></label>
      <input name="title" maxlength="120" required value="${escapeHtml(goal?.title || "")}" placeholder="\u4e00\u8d77\u5b8c\u6210\u7684\u5c0f\u76ee\u6807">
    </div>
    <div class="form-field full">
      <label>\u76ee\u6807\u63cf\u8ff0</label>
      <textarea name="description" rows="3" placeholder="\u5173\u4e8e\u8fd9\u4e2a\u76ee\u6807\u7684\u5c0f\u8ba1\u5212">${escapeHtml(goal?.description || "")}</textarea>
    </div>
    <div class="form-field">
      <label>\u8ba1\u5212\u65e5\u671f</label>
      <input type="date" name="targetDate" value="${escapeHtml(toDateInputValue(goal?.targetDate))}">
    </div>
    <div class="form-field">
      <label>\u76ee\u6807\u72b6\u6001</label>
      <select name="status">
        ${GOAL_STATUS_OPTIONS.map((item) => `<option value="${item.value}" ${item.value === (goal?.status || "PENDING") ? "selected" : ""}>${item.label}</option>`).join("")}
      </select>
    </div>
    <div class="form-field">
      <label>\u5b8c\u6210\u8fdb\u5ea6\uff080-100\uff09</label>
      <input type="number" name="progressRatio" min="0" max="100" value="${goal?.progressRatio ?? 0}">
    </div>
    <div class="form-field">
      <label>\u5206\u7c7b\u6807\u7b7e</label>
      <input name="category" maxlength="60" value="${escapeHtml(goal?.category || "")}" placeholder="\u65c5\u884c / \u5b66\u4e60 / \u751f\u6d3b\u7b49">
    </div>
    <div class="form-field full">
      <label>\u5c01\u9762\u56fe\uff08\u53ef\u4e0a\u4f20\u56fe\u7247\u6216\u89c6\u9891\uff09</label>
      <input type="file" name="coverFile" accept="image/*,video/*">
      <p class="hint">\u82e5\u4e0d\u9009\u62e9\u65b0\u6587\u4ef6\uff0c\u5219\u4fdd\u7559\u539f\u6709\u5c01\u9762\u3002</p>
      <div class="upload-preview" id="goalCoverPreview">
        ${
          goal?.coverPath
            ? `<img src="${escapeHtml(resolveMediaUrl(goal.coverPath))}" alt="\u5c01\u9762\u9884\u89c8">`
            : `<span class="muted">\u5c1a\u672a\u8bbe\u7f6e\u5c01\u9762</span>`
        }
      </div>
    </div>
  `;

  const coverInput = form.querySelector('input[name="coverFile"]');
  const preview = form.querySelector("#goalCoverPreview");
  coverInput.addEventListener("change", () => {
    preview.innerHTML = "";
    if (coverInput.files[0]) {
      const file = coverInput.files[0];
      if (file.type.startsWith("video/")) {
        const video = document.createElement("video");
        video.src = URL.createObjectURL(file);
        video.controls = true;
        preview.appendChild(video);
      } else {
        const img = document.createElement("img");
        img.src = URL.createObjectURL(file);
        img.alt = "\u5c01\u9762\u9884\u89c8";
        preview.appendChild(img);
      }
    } else if (goal?.coverPath) {
      const img = document.createElement("img");
      img.src = resolveMediaUrl(goal.coverPath);
      img.alt = "\u5c01\u9762\u9884\u89c8";
      preview.appendChild(img);
    } else {
      preview.innerHTML = `<span class="muted">\u5c1a\u672a\u8bbe\u7f6e\u5c01\u9762</span>`;
    }
  });

  modal.open({
    title: goal ? "\u7f16\u8f91\u5c0f\u76ee\u6807" : "\u65b0\u589e\u5c0f\u76ee\u6807",
    content: form,
    submitText: "\u4fdd\u5b58",
    onSubmit: async () => {
      if (!form.reportValidity()) {
        return false;
      }
      const payload = {
        title: form.title.value.trim(),
        description: form.description.value.trim() || null,
        targetDate: form.targetDate.value || null,
        status: form.status.value || "PENDING",
        progressRatio: form.progressRatio.value ? Math.min(100, Math.max(0, Number(form.progressRatio.value))) : 0,
        category: form.category.value.trim() || null,
        coverPath: goal?.coverPath || null,
      };

      if (!payload.title) {
        notify("\u8bf7\u586b\u5199\u76ee\u6807\u6807\u9898", "error");
        return false;
      }

      if (coverInput.files[0]) {
        const upload = await uploadFile(coverInput.files[0]);
        payload.coverPath = upload.filePath;
      }

      if (goal) {
        await apiRequest(`/api/goals/${goal.id}`, { method: "PUT", body: payload });
        notify("\u5c0f\u76ee\u6807\u66f4\u65b0\u6210\u529f", "success");
      } else {
        await apiRequest("/api/goals", { method: "POST", body: payload });
        notify("\u5c0f\u76ee\u6807\u521b\u5efa\u6210\u529f", "success");
      }
      state.modules.goals.page = 1;
      await refreshModule("goals");
      renderGoals();
    },
  });
}

async function deleteGoal(goal) {
  if (!confirm(`\u786e\u5b9a\u8981\u5220\u9664\u76ee\u6807\u300c${goal.title || "\u672a\u547d\u540d"}\u300d\u5417\uff1f`)) return;
  try {
    await apiRequest(`/api/goals/${goal.id}`, { method: "DELETE" });
    notify("\u5c0f\u76ee\u6807\u5df2\u5220\u9664", "success");
    await refreshModule("goals");
    renderGoals();
  } catch (error) {
    notify(error.message || "\u5220\u9664\u76ee\u6807\u5931\u8d25", "error");
  }
}

// \u6bcf\u65e5\u65e5\u7a0b\u8868\u5355\u5f39\u7a97
function openRoutineForm(routine) {
  const form = document.createElement("form");
  form.className = "form-grid";
  form.innerHTML = `
    <div class="form-field">
      <label>\u65f6\u95f4\u6bb5 <span class="required">*</span></label>
      <input name="timeRange" required value="${escapeHtml(routine?.timeRange || "")}" placeholder="\u4f8b\u5982 07:30-08:00">
    </div>
    <div class="form-field">
      <label>\u6807\u7b7e</label>
      <input name="label" value="${escapeHtml(routine?.label || "")}" placeholder="\u4f8b\u5982 \u65e9\u9910 / \u901a\u52e4">
    </div>
    <div class="form-field full">
      <label>\u6d3b\u52a8\u63cf\u8ff0 <span class="required">*</span></label>
      <textarea name="description" rows="3" required placeholder="\u5199\u4e0b\u8fd9\u4e2a\u65f6\u95f4\u6bb5\u8981\u505a\u7684\u4e8b">${escapeHtml(routine?.description || "")}</textarea>
    </div>
    <div class="form-field">
      <label>\u5fc3\u60c5\u8868\u60c5</label>
      <input name="emoji" value="${escapeHtml(routine?.emoji || "")}" placeholder="\u4f8b\u5982 \u2615\ufe0f / \u1f31e">
    </div>
    <div class="form-field">
      <label>\u6392\u5e8f\u6743\u91cd</label>
      <input type="number" name="sortOrder" value="${routine?.sortOrder ?? 0}">
    </div>
    <div class="form-field full">
      <label>\u80cc\u666f\u56fe</label>
      <input type="file" name="backgroundFile" accept="image/*,video/*">
      <div class="upload-preview" id="routineCoverPreview">
        ${
          routine?.backgroundPath
            ? `<img src="${escapeHtml(resolveMediaUrl(routine.backgroundPath))}" alt="\u80cc\u666f\u9884\u89c8">`
            : `<span class="muted">\u5c1a\u672a\u8bbe\u7f6e\u80cc\u666f</span>`
        }
      </div>
    </div>
  `;

  const coverInput = form.querySelector('input[name="backgroundFile"]');
  const preview = form.querySelector("#routineCoverPreview");
  coverInput.addEventListener("change", () => {
    preview.innerHTML = "";
    if (coverInput.files[0]) {
      const file = coverInput.files[0];
      if (file.type.startsWith("video/")) {
        const video = document.createElement("video");
        video.controls = true;
        video.src = URL.createObjectURL(file);
        preview.appendChild(video);
      } else {
        const img = document.createElement("img");
        img.src = URL.createObjectURL(file);
        img.alt = "\u80cc\u666f\u9884\u89c8";
        preview.appendChild(img);
      }
    } else if (routine?.backgroundPath) {
      const img = document.createElement("img");
      img.src = resolveMediaUrl(routine.backgroundPath);
      img.alt = "\u80cc\u666f\u9884\u89c8";
      preview.appendChild(img);
    } else {
      preview.innerHTML = `<span class="muted">\u5c1a\u672a\u8bbe\u7f6e\u80cc\u666f</span>`;
    }
  });

  modal.open({
    title: routine ? "\u7f16\u8f91\u65e5\u7a0b" : "\u65b0\u589e\u65e5\u7a0b",
    content: form,
    submitText: "\u4fdd\u5b58",
    onSubmit: async () => {
      if (!form.reportValidity()) {
        return false;
      }
      const payload = {
        timeRange: form.timeRange.value.trim(),
        label: form.label.value.trim() || null,
        description: form.description.value.trim(),
        emoji: form.emoji.value.trim() || null,
        sortOrder: form.sortOrder.value ? Number(form.sortOrder.value) : 0,
        backgroundPath: routine?.backgroundPath || null,
      };

      if (!payload.timeRange || !payload.description) {
        notify("\u8bf7\u586b\u5199\u65f6\u95f4\u6bb5\u548c\u6d3b\u52a8\u5185\u5bb9", "error");
        return false;
      }

      if (coverInput.files[0]) {
        const upload = await uploadFile(coverInput.files[0]);
        payload.backgroundPath = upload.filePath;
      }

      if (routine) {
        await apiRequest(`/api/routines/${routine.id}`, { method: "PUT", body: payload });
        notify("\u65e5\u7a0b\u5df2\u66f4\u65b0", "success");
      } else {
        await apiRequest("/api/routines", { method: "POST", body: payload });
        notify("\u65e5\u7a0b\u5df2\u521b\u5efa", "success");
      }
      await refreshModule("routines");
      renderRoutines();
    },
  });
}

async function deleteRoutine(routine) {
  if (!confirm(`\u786e\u5b9a\u8981\u5220\u9664\u65e5\u7a0b\u300c${routine.label || routine.timeRange}\u300d\u5417\uff1f`)) return;
  try {
    await apiRequest(`/api/routines/${routine.id}`, { method: "DELETE" });
    notify("\u65e5\u7a0b\u5df2\u5220\u9664", "success");
    await refreshModule("routines");
    renderRoutines();
  } catch (error) {
    notify(error.message || "\u5220\u9664\u5931\u8d25", "error");
  }
}

// \u751f\u6d3b\u77ac\u95f4\uff08\u5305\u542b\u9644\u4ef6\u4e0a\u4f20\uff09
function openLifeForm(record) {
  const form = document.createElement("form");
  form.className = "form-grid";
  form.innerHTML = `
    <div class="form-field">
      <label>\u8bb0\u5f55\u65e5\u671f <span class="required">*</span></label>
      <input type="date" name="recordDate" required value="${escapeHtml(toDateInputValue(record?.recordDate))}">
    </div>
    <div class="form-field">
      <label>\u6807\u9898 <span class="required">*</span></label>
      <input name="title" required maxlength="120" value="${escapeHtml(record?.title || "")}" placeholder="\u8fd9\u4e00\u5929\u7684\u4e3b\u9898">
    </div>
    <div class="form-field">
      <label>\u5730\u70b9</label>
      <input name="location" value="${escapeHtml(record?.location || "")}">
    </div>
    <div class="form-field">
      <label>\u5929\u6c14</label>
      <input name="weather" value="${escapeHtml(record?.weather || "")}">
    </div>
    <div class="form-field">
      <label>\u5fc3\u60c5</label>
      <input name="mood" value="${escapeHtml(record?.mood || "")}">
    </div>
    <div class="form-field full">
      <label>\u4e8b\u4ef6\u63cf\u8ff0</label>
      <textarea name="description" rows="3" placeholder="\u5199\u4e0b\u503c\u5f97\u7eaa\u5ff5\u7684\u7ec6\u8282">${escapeHtml(record?.description || "")}</textarea>
    </div>
    <div class="form-field full">
      <label>\u5c01\u9762\u56fe</label>
      <input type="file" name="coverFile" accept="image/*,video/*">
      <div class="upload-preview" id="lifeCoverPreview">
        ${
          record?.coverPath
            ? `<img src="${escapeHtml(resolveMediaUrl(record.coverPath))}" alt="\u5c01\u9762\u9884\u89c8">`
            : `<span class="muted">\u5c1a\u672a\u8bbe\u7f6e\u5c01\u9762</span>`
        }
      </div>
    </div>
    <div class="form-field full">
      <label>\u9644\u4ef6\uff08\u53ef\u9009\uff09</label>
      <input type="file" name="attachmentFile" accept="image/*,video/*,audio/*">
      <p class="hint">\u7528\u4e8e\u4fdd\u5b58\u539f\u59cb\u6587\u4ef6\uff0c\u4fbf\u4e8e\u540e\u7eed\u4e0b\u8f7d\u6216\u67e5\u770b\u3002</p>
    </div>
  `;

  const coverInput = form.querySelector('input[name="coverFile"]');
  const coverPreview = form.querySelector("#lifeCoverPreview");
  coverInput.addEventListener("change", () => {
    coverPreview.innerHTML = "";
    if (coverInput.files[0]) {
      const file = coverInput.files[0];
      if (file.type.startsWith("video/")) {
        const video = document.createElement("video");
        video.controls = true;
        video.src = URL.createObjectURL(file);
        coverPreview.appendChild(video);
      } else {
        const img = document.createElement("img");
        img.src = URL.createObjectURL(file);
        img.alt = "\u5c01\u9762\u9884\u89c8";
        coverPreview.appendChild(img);
      }
    } else if (record?.coverPath) {
      const img = document.createElement("img");
      img.src = resolveMediaUrl(record.coverPath);
      img.alt = "\u5c01\u9762\u9884\u89c8";
      coverPreview.appendChild(img);
    } else {
      coverPreview.innerHTML = `<span class="muted">\u5c1a\u672a\u8bbe\u7f6e\u5c01\u9762</span>`;
    }
  });

  modal.open({
    title: record ? "\u7f16\u8f91\u751f\u6d3b\u77ac\u95f4" : "\u65b0\u589e\u751f\u6d3b\u77ac\u95f4",
    content: form,
    submitText: "\u4fdd\u5b58",
    onSubmit: async () => {
      if (!form.reportValidity()) {
        return false;
      }
      const payload = {
        recordDate: form.recordDate.value,
        title: form.title.value.trim(),
        location: form.location.value.trim() || null,
        weather: form.weather.value.trim() || null,
        mood: form.mood.value.trim() || null,
        description: form.description.value.trim() || null,
        coverPath: record?.coverPath || null,
        attachmentId: record?.attachmentId || null,
      };

      if (!payload.recordDate || !payload.title) {
        notify("\u8bf7\u586b\u5199\u65e5\u671f\u548c\u6807\u9898", "error");
        return false;
      }

      if (coverInput.files[0]) {
        const upload = await uploadFile(coverInput.files[0]);
        payload.coverPath = upload.filePath;
      }

      const attachmentFile = form.attachmentFile.files[0];
      if (attachmentFile) {
        const upload = await uploadFile(attachmentFile);
        payload.attachmentId = upload.id;
        if (!payload.coverPath) {
          payload.coverPath = upload.filePath;
        }
      }

      if (record) {
        await apiRequest(`/api/life-records/${record.id}`, { method: "PUT", body: payload });
        notify("\u751f\u6d3b\u77ac\u95f4\u5df2\u66f4\u65b0", "success");
      } else {
        await apiRequest("/api/life-records", { method: "POST", body: payload });
        notify("\u751f\u6d3b\u77ac\u95f4\u5df2\u8bb0\u5f55", "success");
      }
      await refreshModule("life");
      renderLifeMoments();
    },
  });
}

async function deleteLifeRecord(record) {
  if (!confirm(`\u786e\u5b9a\u8981\u5220\u9664\u300a${record.title || "\u672a\u547d\u540d\u77ac\u95f4"}\u300b\u5417\uff1f`)) return;
  try {
    await apiRequest(`/api/life-records/${record.id}`, { method: "DELETE" });
    notify("\u751f\u6d3b\u77ac\u95f4\u5df2\u5220\u9664", "success");
    await refreshModule("life");
    renderLifeMoments();
  } catch (error) {
    notify(error.message || "\u5220\u9664\u5931\u8d25", "error");
  }
}
// \u751c\u871c\u65e5\u5fd7\u8868\u5355\uff0c\u652f\u6301\u591a\u5a92\u4f53\u9644\u4ef6
function openDiaryForm(diary) {
  const form = document.createElement("form");
  form.className = "form-grid";
  form.innerHTML = `
    <div class="form-field">
      <label>\u5199\u4f5c\u65e5\u671f <span class="required">*</span></label>
      <input type="date" name="entryDate" required value="${escapeHtml(toDateInputValue(diary?.entryDate || new Date()))}">
    </div>
    <div class="form-field">
      <label>\u6807\u9898</label>
      <input name="title" maxlength="120" value="${escapeHtml(diary?.title || "")}" placeholder="\u751c\u871c\u65e5\u8bb0\u6807\u9898">
    </div>
    <div class="form-field">
      <label>\u5929\u6c14</label>
      <input name="weather" value="${escapeHtml(diary?.weather || "")}">
    </div>
    <div class="form-field">
      <label>\u5fc3\u60c5</label>
      <input name="mood" value="${escapeHtml(diary?.mood || "")}">
    </div>
    <div class="form-field full">
      <label>\u6b63\u6587\u5185\u5bb9</label>
      <textarea name="content" rows="6" placeholder="\u5199\u4e0b\u4eca\u5929\u7684\u6545\u4e8b">${escapeHtml(diary?.content || "")}</textarea>
    </div>
    <div class="form-field full">
      <label>\u611f\u6069\u8bb0\u5f55</label>
      <textarea name="gratitude" rows="3" placeholder="\u503c\u5f97\u611f\u6069\u4e0e\u73cd\u85cf\u7684\u77ac\u95f4">${escapeHtml(diary?.gratitude || "")}</textarea>
    </div>
    <div class="form-field full">
      <label>\u5c01\u9762\u56fe</label>
      <input type="file" name="coverFile" accept="image/*,video/*">
      <div class="upload-preview" id="diaryCoverPreview">
        ${
          diary?.coverPath
            ? `<img src="${escapeHtml(resolveMediaUrl(diary.coverPath))}" alt="\u5c01\u9762\u9884\u89c8">`
            : `<span class="muted">\u5c1a\u672a\u8bbe\u7f6e\u5c01\u9762</span>`
        }
      </div>
    </div>
    <div class="form-field full">
      <label>\u591a\u5a92\u4f53\u9644\u4ef6\uff08\u53ef\u591a\u9009\uff09</label>
      <input type="file" name="mediaFiles" accept="image/*,video/*" multiple>
      <p class="hint">\u652f\u6301\u56fe\u7247\u6216\u89c6\u9891\uff0c\u53ef\u4e00\u6b21\u9009\u62e9\u591a\u5f20\u3002</p>
    </div>
    <div class="form-field full" id="existingMediaContainer">
      ${
        diary?.id
          ? `<label>\u5df2\u4fdd\u5b58\u7684\u9644\u4ef6</label>
             <div class="media-list">
               <p class="muted">\u5b8c\u5584\u9644\u4ef6\u8bf4\u660e\u540e\u4fdd\u5b58\u5373\u53ef\u66f4\u65b0\u3002</p>
             </div>`
          : ""
      }
    </div>
  `;

  let existingMedia = [];

  const coverPreview = form.querySelector("#diaryCoverPreview");
  const coverInput = form.querySelector('input[name="coverFile"]');
  coverInput.addEventListener("change", () => {
    coverPreview.innerHTML = "";
    if (coverInput.files[0]) {
      const file = coverInput.files[0];
      if (file.type.startsWith("video/")) {
        const video = document.createElement("video");
        video.controls = true;
        video.src = URL.createObjectURL(file);
        coverPreview.appendChild(video);
      } else {
        const img = document.createElement("img");
        img.src = URL.createObjectURL(file);
        img.alt = "\u5c01\u9762\u9884\u89c8";
        coverPreview.appendChild(img);
      }
    } else if (diary?.coverPath) {
      const img = document.createElement("img");
      img.src = resolveMediaUrl(diary.coverPath);
      img.alt = "\u5c01\u9762\u9884\u89c8";
      coverPreview.appendChild(img);
    } else {
      coverPreview.innerHTML = `<span class="muted">\u5c1a\u672a\u8bbe\u7f6e\u5c01\u9762</span>`;
    }
  });

  modal.open({
    title: diary ? "\u7f16\u8f91\u751c\u871c\u65e5\u8bb0" : "\u64b0\u5199\u751c\u871c\u65e5\u8bb0",
    content: form,
    submitText: "\u4fdd\u5b58",
    onSubmit: async () => {
      if (!form.reportValidity()) {
        return false;
      }
      const payload = {
        entryDate: form.entryDate.value,
        title: form.title.value.trim() || null,
        weather: form.weather.value.trim() || null,
        mood: form.mood.value.trim() || null,
        content: form.content.value.trim() || null,
        gratitude: form.gratitude.value.trim() || null,
        coverPath: diary?.coverPath || null,
        media: [],
      };

      if (!payload.entryDate) {
        notify("\u8bf7\u9009\u62e9\u65e5\u8bb0\u65e5\u671f", "error");
        return false;
      }

      if (coverInput.files[0]) {
        const upload = await uploadFile(coverInput.files[0]);
        payload.coverPath = upload.filePath;
      }

      existingMedia.forEach((item, index) => {
        payload.media.push({
          id: item.id,
          filePath: item.filePath,
          mediaType: item.mediaType,
          caption: item.captionInput?.value.trim() || item.caption || "",
          sortOrder: index,
        });
      });

      const files = Array.from(form.mediaFiles.files || []);
      if (files.length) {
        const uploads = await Promise.all(files.map((file) => uploadFile(file)));
        uploads.forEach((meta, idx) => {
          payload.media.push({
            filePath: meta.filePath,
            mediaType: (meta.mediaType || "").toUpperCase().includes("VIDEO") ? "VIDEO" : "IMAGE",
            caption: "",
            sortOrder: existingMedia.length + idx,
          });
        });
      }

      if (diary) {
        await apiRequest(`/api/diaries/${diary.id}`, { method: "PUT", body: payload });
        notify("\u65e5\u8bb0\u5df2\u66f4\u65b0", "success");
      } else {
        await apiRequest("/api/diaries", { method: "POST", body: payload });
        notify("\u65e5\u8bb0\u5df2\u4fdd\u5b58", "success");
      }
      await refreshModule("diaries");
      renderDiaries();
    },
  });

  if (diary?.id) {
    loadDiaryDetail(diary.id).then((detail) => {
      existingMedia = detail.media || [];
      const mediaContainer = form.querySelector("#existingMediaContainer .media-list");
      if (!mediaContainer) return;
      if (!existingMedia.length) {
        mediaContainer.innerHTML = `<div class="muted">\u6682\u65e0\u9644\u4ef6</div>`;
        return;
      }
      mediaContainer.innerHTML = existingMedia
        .map((media) => {
          const preview = media.mediaType === "VIDEO"
            ? `<video controls src="${escapeHtml(resolveMediaUrl(media.filePath))}"></video>`
            : `<img src="${escapeHtml(resolveMediaUrl(media.filePath))}" alt="\u9644\u4ef6\u9884\u89c8">`;
          return `
            <div class="media-item" data-id="${media.id}">
              ${preview}
              <input class="caption-input" placeholder="\u9644\u4ef6\u8bf4\u660e" value="${escapeHtml(media.caption || "")}">
            </div>
          `;
        })
        .join("");
      existingMedia.forEach((media) => {
        const node = mediaContainer.querySelector(`[data-id="${media.id}"] .caption-input`);
        media.captionInput = node;
      });
    }).catch((error) => {
      console.error(error);
      notify(error.message || "\u52a0\u8f7d\u65e5\u8bb0\u8be6\u60c5\u5931\u8d25", "error");
    });
  }
}

async function loadDiaryDetail(id) {
  return apiRequest(`/api/diaries/${id}`);
}

async function openDiaryDetail(id) {
  try {
    const detail = await loadDiaryDetail(id);
    const entry = detail.entry || {};
    const mediaList = (detail.media || []).map((item) => {
      const isVideo = String(item.mediaType || "").toUpperCase().includes("VIDEO");
      return `
        <div class="media-item">
          ${isVideo
            ? `<video controls src="${escapeHtml(resolveMediaUrl(item.filePath))}"></video>`
            : `<img src="${escapeHtml(resolveMediaUrl(item.filePath))}" alt="\u9644\u4ef6">`}
          <p>${escapeHtml(item.caption || "")}</p>
        </div>
      `;
    }).join("");

    modal.open({
      title: entry.title || "\u751c\u871c\u65e5\u8bb0",
      content: `
        <div class="diary-detail">
          <div class="diary-meta">
            <span>${formatDate(entry.entryDate)}</span>
            <span>${entry.weather ? `\u5929\u6c14\uff1a${escapeHtml(entry.weather)}` : ""}</span>
            <span>${entry.mood ? `\u5fc3\u60c5\uff1a${escapeHtml(entry.mood)}` : ""}</span>
          </div>
          <article class="diary-content">
            <p>${escapeHtml(entry.content || "\u6682\u65e0\u6b63\u6587")}</p>
            ${entry.gratitude ? `<h4>\u611f\u6069\u8bb0\u5f55</h4><p>${escapeHtml(entry.gratitude)}</p>` : ""}
          </article>
          <div class="media-list">
            ${mediaList || `<div class="muted">\u6682\u65e0\u9644\u4ef6</div>`}
          </div>
        </div>
      `,
      submitText: "\u5173\u95ed",
      hideCancel: true,
      onSubmit: () => true,
    });
  } catch (error) {
    notify(error.message || "\u67e5\u770b\u65e5\u8bb0\u5931\u8d25", "error");
  }
}

async function deleteDiary(diary) {
  if (!confirm(`\u786e\u5b9a\u8981\u5220\u9664\u65e5\u8bb0\u300c${diary.title || formatDate(diary.entryDate)}\u300d\u5417\uff1f`)) return;
  try {
    await apiRequest(`/api/diaries/${diary.id}`, { method: "DELETE" });
    notify("\u65e5\u8bb0\u5df2\u5220\u9664", "success");
    await refreshModule("diaries");
    renderDiaries();
  } catch (error) {
    notify(error.message || "\u5220\u9664\u5931\u8d25", "error");
  }
}
// \u6e29\u6696\u63d0\u9192\u8868\u5355\uff0c\u5305\u542b\u786e\u8ba4\u72b6\u6001\u4e0e\u914d\u56fe
function openReminderForm(reminder) {
  const form = document.createElement("form");
  form.className = "form-grid";
  form.innerHTML = `
    <div class="form-field full">
      <label>\u63d0\u9192\u6807\u9898 <span class="required">*</span></label>
      <input name="title" required maxlength="120" value="${escapeHtml(reminder?.title || "")}" placeholder="\u8bb0\u5f97\u505a\u4ec0\u4e48\u4e8b">
    </div>
    <div class="form-field full">
      <label>\u63d0\u9192\u5185\u5bb9</label>
      <textarea name="message" rows="3" placeholder="\u5199\u4e0b\u63d0\u9192\u8be6\u60c5">${escapeHtml(reminder?.message || "")}</textarea>
    </div>
    <div class="form-field">
      <label>\u63d0\u9192\u65f6\u95f4</label>
      <input type="datetime-local" name="remindAt" value="${escapeHtml(toDateTimeInputValue(reminder?.remindAt))}">
    </div>
    <div class="form-field">
      <label>\u4f18\u5148\u7ea7</label>
      <select name="priority">
        ${REMINDER_PRIORITY_OPTIONS.map((item) => `<option value="${item.value}" ${item.value === (reminder?.priority || "MEDIUM") ? "selected" : ""}>${item.label}</option>`).join("")}
      </select>
    </div>
    <div class="form-field">
      <label>\u7c7b\u522b\u6807\u7b7e</label>
      <input name="category" value="${escapeHtml(reminder?.category || "")}" placeholder="\u7eaa\u5ff5\u65e5 / \u5f85\u529e / \u60f3\u5ff5">
    </div>
    <div class="form-field">
      <label>\u662f\u5426\u5df2\u786e\u8ba4</label>
      <label class="checkbox">
        <input type="checkbox" name="acknowledged" ${reminder?.acknowledged ? "checked" : ""}>
        \u5df2\u7ecf\u5b8c\u6210\u6216\u786e\u8ba4
      </label>
    </div>
    <div class="form-field full">
      <label>\u914d\u56fe</label>
      <input type="file" name="coverFile" accept="image/*,video/*">
      <div class="upload-preview" id="reminderCoverPreview">
        ${
          reminder?.coverPath
            ? `<img src="${escapeHtml(resolveMediaUrl(reminder.coverPath))}" alt="\u914d\u56fe\u9884\u89c8">`
            : `<span class="muted">\u5c1a\u672a\u8bbe\u7f6e\u914d\u56fe</span>`
        }
      </div>
    </div>
  `;

  const coverInput = form.querySelector('input[name="coverFile"]');
  const coverPreview = form.querySelector("#reminderCoverPreview");
  coverInput.addEventListener("change", () => {
    coverPreview.innerHTML = "";
    if (coverInput.files[0]) {
      const file = coverInput.files[0];
      if (file.type.startsWith("video/")) {
        const video = document.createElement("video");
        video.controls = true;
        video.src = URL.createObjectURL(file);
        coverPreview.appendChild(video);
      } else {
        const img = document.createElement("img");
        img.src = URL.createObjectURL(file);
        img.alt = "\u914d\u56fe\u9884\u89c8";
        coverPreview.appendChild(img);
      }
    } else if (reminder?.coverPath) {
      const img = document.createElement("img");
      img.src = resolveMediaUrl(reminder.coverPath);
      img.alt = "\u914d\u56fe\u9884\u89c8";
      coverPreview.appendChild(img);
    } else {
      coverPreview.innerHTML = `<span class="muted">\u5c1a\u672a\u8bbe\u7f6e\u914d\u56fe</span>`;
    }
  });

  modal.open({
    title: reminder ? "\u7f16\u8f91\u6e29\u6696\u63d0\u9192" : "\u65b0\u589e\u6e29\u6696\u63d0\u9192",
    content: form,
    submitText: "\u4fdd\u5b58",
    onSubmit: async () => {
      if (!form.reportValidity()) {
        return false;
      }
      const payload = {
        title: form.title.value.trim(),
        message: form.message.value.trim() || null,
        remindAt: normalizeDateTimeValue(form.remindAt.value),
        priority: form.priority.value || "MEDIUM",
        category: form.category.value.trim() || null,
        acknowledged: form.acknowledged.checked,
        coverPath: reminder?.coverPath || null,
      };

      if (!payload.title) {
        notify("\u8bf7\u586b\u5199\u63d0\u9192\u6807\u9898", "error");
        return false;
      }

      if (coverInput.files[0]) {
        const upload = await uploadFile(coverInput.files[0]);
        payload.coverPath = upload.filePath;
      }

      if (reminder) {
        await apiRequest(`/api/reminders/${reminder.id}`, { method: "PUT", body: payload });
        notify("\u63d0\u9192\u5df2\u66f4\u65b0", "success");
      } else {
        await apiRequest("/api/reminders", { method: "POST", body: payload });
        notify("\u63d0\u9192\u5df2\u521b\u5efa", "success");
      }
      await refreshModule("reminders");
      renderReminders();
    },
  });
}

async function deleteReminder(reminder) {
  if (!confirm(`\u786e\u5b9a\u8981\u5220\u9664\u63d0\u9192\u300c${reminder.title || ""}\u300d\u5417\uff1f`)) return;
  try {
    await apiRequest(`/api/reminders/${reminder.id}`, { method: "DELETE" });
    notify("\u63d0\u9192\u5df2\u5220\u9664", "success");
    await refreshModule("reminders");
    renderReminders();
  } catch (error) {
    notify(error.message || "\u5220\u9664\u5931\u8d25", "error");
  }
}

async function acknowledgeReminder(reminder) {
  try {
    await apiRequest(`/api/reminders/${reminder.id}/ack`, { method: "PATCH" });
    notify("\u63d0\u9192\u72b6\u6001\u5df2\u66f4\u65b0", "success");
    await refreshModule("reminders");
    renderReminders();
  } catch (error) {
    notify(error.message || "\u66f4\u65b0\u72b6\u6001\u5931\u8d25", "error");
  }
}

// \u6536\u85cf\u8bed\u5f55\u8868\u5355
function openQuoteForm(quote) {
  const form = document.createElement("form");
  form.className = "form-grid";
  form.innerHTML = `
    <div class="form-field full">
      <label>\u8bed\u5f55\u5185\u5bb9 <span class="required">*</span></label>
      <textarea name="content" rows="3" required placeholder="\u5199\u4e0b\u4e00\u53e5\u6253\u52a8\u4eba\u5fc3\u7684\u8bdd">${escapeHtml(quote?.content || "")}</textarea>
    </div>
    <div class="form-field">
      <label>\u6765\u6e90</label>
      <input name="source" value="${escapeHtml(quote?.source || "")}" placeholder="\u51fa\u81ea\u54ea\u91cc">
    </div>
    <div class="form-field">
      <label>\u5fc3\u60c5\u6807\u7b7e</label>
      <input name="moodTag" value="${escapeHtml(quote?.moodTag || "")}" placeholder="\u4f8b\u5982 \u9f13\u52b1 / \u6e29\u67d4">
    </div>
    <div class="form-field full">
      <label>\u914d\u56fe</label>
      <input type="file" name="coverFile" accept="image/*,video/*">
      <div class="upload-preview" id="quoteCoverPreview">
        ${
          quote?.coverPath
            ? `<img src="${escapeHtml(resolveMediaUrl(quote.coverPath))}" alt="\u914d\u56fe\u9884\u89c8">`
            : `<span class="muted">\u5c1a\u672a\u8bbe\u7f6e\u914d\u56fe</span>`
        }
      </div>
    </div>
  `;

  const coverInput = form.querySelector('input[name="coverFile"]');
  const coverPreview = form.querySelector("#quoteCoverPreview");
  coverInput.addEventListener("change", () => {
    coverPreview.innerHTML = "";
    if (coverInput.files[0]) {
      const file = coverInput.files[0];
      if (file.type.startsWith("video/")) {
        const video = document.createElement("video");
        video.controls = true;
        video.src = URL.createObjectURL(file);
        coverPreview.appendChild(video);
      } else {
        const img = document.createElement("img");
        img.src = URL.createObjectURL(file);
        img.alt = "\u914d\u56fe\u9884\u89c8";
        coverPreview.appendChild(img);
      }
    } else if (quote?.coverPath) {
      const img = document.createElement("img");
      img.src = resolveMediaUrl(quote.coverPath);
      img.alt = "\u914d\u56fe\u9884\u89c8";
      coverPreview.appendChild(img);
    } else {
      coverPreview.innerHTML = `<span class="muted">\u5c1a\u672a\u8bbe\u7f6e\u914d\u56fe</span>`;
    }
  });

  modal.open({
    title: quote ? "\u7f16\u8f91\u8bed\u5f55" : "\u65b0\u589e\u8bed\u5f55",
    content: form,
    submitText: "\u4fdd\u5b58",
    onSubmit: async () => {
      if (!form.reportValidity()) {
        return false;
      }
      const payload = {
        content: form.content.value.trim(),
        source: form.source.value.trim() || null,
        moodTag: form.moodTag.value.trim() || null,
        coverPath: quote?.coverPath || null,
      };
      if (!payload.content) {
        notify("\u8bf7\u586b\u5199\u8bed\u5f55\u5185\u5bb9", "error");
        return false;
      }
      if (coverInput.files[0]) {
        const upload = await uploadFile(coverInput.files[0]);
        payload.coverPath = upload.filePath;
      }
      if (quote) {
        await apiRequest(`/api/quotes/${quote.id}`, { method: "PUT", body: payload });
        notify("\u8bed\u5f55\u5df2\u66f4\u65b0", "success");
      } else {
        await apiRequest("/api/quotes", { method: "POST", body: payload });
        notify("\u8bed\u5f55\u5df2\u6536\u85cf", "success");
      }
      await refreshModule("quotes");
      renderQuotes();
    },
  });
}

async function deleteQuote(quote) {
  if (!confirm("\u786e\u5b9a\u8981\u5220\u9664\u8fd9\u6761\u8bed\u5f55\u5417\uff1f")) return;
  try {
    await apiRequest(`/api/quotes/${quote.id}`, { method: "DELETE" });
    notify("\u8bed\u5f55\u5df2\u5220\u9664", "success");
    await refreshModule("quotes");
    renderQuotes();
  } catch (error) {
    notify(error.message || "\u5220\u9664\u5931\u8d25", "error");
  }
}

// \u5f69\u8272\u6807\u7b7e\u8868\u5355
function openNoteForm(note) {
  const form = document.createElement("form");
  form.className = "form-grid";
  form.innerHTML = `
    <div class="form-field">
      <label>\u6807\u9898</label>
      <input name="title" maxlength="120" value="${escapeHtml(note?.title || "")}" placeholder="\u4fbf\u7b7e\u6807\u9898">
    </div>
    <div class="form-field">
      <label>\u4e3b\u9898\u989c\u8272</label>
      <input type="color" name="colorHex" value="${escapeHtml(note?.colorHex || "#ffd6e8")}">
    </div>
    <div class="form-field full">
      <label>\u4fbf\u7b7e\u5185\u5bb9 <span class="required">*</span></label>
      <textarea name="content" rows="3" required placeholder="\u5199\u4e0b\u5173\u952e\u4e8b\u60c5">${escapeHtml(note?.content || "")}</textarea>
    </div>
    <div class="form-field">
      <label>\u662f\u5426\u7f6e\u9876</label>
      <label class="checkbox">
        <input type="checkbox" name="pinned" ${note?.pinned ? "checked" : ""}>
        \u7f6e\u9876\u663e\u793a
      </label>
    </div>
    <div class="form-field">
      <label>\u8fc7\u671f\u65f6\u95f4</label>
      <input type="datetime-local" name="expiresAt" value="${escapeHtml(toDateTimeInputValue(note?.expiresAt))}">
    </div>
    <div class="form-field full">
      <label>\u80cc\u666f\u914d\u56fe</label>
      <input type="file" name="coverFile" accept="image/*,video/*">
      <div class="upload-preview" id="noteCoverPreview">
        ${
          note?.coverPath
            ? `<img src="${escapeHtml(resolveMediaUrl(note.coverPath))}" alt="\u80cc\u666f\u9884\u89c8">`
            : `<span class="muted">\u5c1a\u672a\u8bbe\u7f6e\u80cc\u666f</span>`
        }
      </div>
    </div>
  `;

  const coverInput = form.querySelector('input[name="coverFile"]');
  const coverPreview = form.querySelector("#noteCoverPreview");
  coverInput.addEventListener("change", () => {
    coverPreview.innerHTML = "";
    if (coverInput.files[0]) {
      const file = coverInput.files[0];
      if (file.type.startsWith("video/")) {
        const video = document.createElement("video");
        video.controls = true;
        video.src = URL.createObjectURL(file);
        coverPreview.appendChild(video);
      } else {
        const img = document.createElement("img");
        img.src = URL.createObjectURL(file);
        img.alt = "\u80cc\u666f\u9884\u89c8";
        coverPreview.appendChild(img);
      }
    } else if (note?.coverPath) {
      const img = document.createElement("img");
      img.src = resolveMediaUrl(note.coverPath);
      img.alt = "\u80cc\u666f\u9884\u89c8";
      coverPreview.appendChild(img);
    } else {
      coverPreview.innerHTML = `<span class="muted">\u5c1a\u672a\u8bbe\u7f6e\u80cc\u666f</span>`;
    }
  });

  modal.open({
    title: note ? "\u7f16\u8f91\u4fbf\u7b7e" : "\u65b0\u589e\u4fbf\u7b7e",
    content: form,
    submitText: "\u4fdd\u5b58",
    onSubmit: async () => {
      if (!form.reportValidity()) {
        return false;
      }
      const payload = {
        title: form.title.value.trim() || null,
        content: form.content.value.trim(),
        colorHex: form.colorHex.value || "#ffd6e8",
        pinned: form.pinned.checked,
        expiresAt: normalizeDateTimeValue(form.expiresAt.value),
        coverPath: note?.coverPath || null,
      };
      if (!payload.content) {
        notify("\u8bf7\u586b\u5199\u4fbf\u7b7e\u5185\u5bb9", "error");
        return false;
      }
      if (coverInput.files[0]) {
        const upload = await uploadFile(coverInput.files[0]);
        payload.coverPath = upload.filePath;
      }
      if (note) {
        await apiRequest(`/api/notes/${note.id}`, { method: "PUT", body: payload });
        notify("\u4fbf\u7b7e\u5df2\u66f4\u65b0", "success");
      } else {
        await apiRequest("/api/notes", { method: "POST", body: payload });
        notify("\u4fbf\u7b7e\u5df2\u521b\u5efa", "success");
      }
      await refreshModule("notes");
      renderNotes();
    },
  });
}

async function deleteNote(note) {
  const rawTitle = typeof note?.title === "string" ? note.title.trim() : "";
  const decoratedTitle = rawTitle ? `\u201c${rawTitle}\u201d` : "\u8fd9\u6761\u4fbf\u7b7e";
  const confirmed = await openConfirmDialog({
    title: "\u786e\u8ba4\u5220\u9664",
    message: `\u786e\u5b9a\u8981\u5220\u9664${escapeHtml(decoratedTitle)}\u5417\uff1f`,
    note: "\u5220\u9664\u540e\u65e0\u6cd5\u6062\u590d\uff0c\u8bf7\u8c28\u614e\u64cd\u4f5c\u3002",
    confirmText: "\u786e\u8ba4\u5220\u9664",
  });
  if (!confirmed) {
    return;
  }
  try {
    await apiRequest(`/api/notes/${note.id}`, { method: "DELETE" });
    notify("\u4fbf\u7b7e\u5df2\u5220\u9664", "success");
    await refreshModule("notes");
    renderNotes();
  } catch (error) {
    notify(error.message || "\u5220\u9664\u5931\u8d25", "error");
  }
}

// \u7231\u7684\u76f8\u518c\u65b0\u589e\u5f39\u7a97\uff08\u6682\u4e0d\u652f\u6301\u76f4\u63a5\u7f16\u8f91\uff09
function openAlbumForm(media) {
  const form = document.createElement("form");
  form.className = "form-grid";
  form.innerHTML = `
    <div class="form-field">
      <label>\u6807\u9898</label>
      <input name="title" maxlength="120" value="${escapeHtml(media?.title || "")}" placeholder="\u56de\u5fc6\u6807\u9898">
    </div>
    <div class="form-field">
      <label>\u62cd\u6444\u65f6\u95f4</label>
      <input type="datetime-local" name="takenAt" value="${escapeHtml(toDateTimeInputValue(media?.takenAt))}">
    </div>
    <div class="form-field">
      <label>\u5fc3\u60c5</label>
      <input name="mood" value="${escapeHtml(media?.mood || "")}">
    </div>
    <div class="form-field full">
      <label>\u63cf\u8ff0</label>
      <textarea name="description" rows="3" placeholder="\u5199\u4e0b\u8fd9\u6bb5\u56de\u5fc6\u7684\u610f\u4e49">${escapeHtml(media?.description || "")}</textarea>
    </div>
    <div class="form-field full">
      <label>${media ? "\u66ff\u6362\u6587\u4ef6\uff08\u4e0d\u9009\u5219\u4fdd\u7559\u539f\u6587\u4ef6\uff09" : "\u7167\u7247/\u89c6\u9891\u6587\u4ef6 <span class='required'>*</span>"}</label>
      <input type="file" name="file" accept="image/*,video/*" ${media ? "" : "required"}>
      <div class="upload-preview" id="albumFilePreview">
        ${
          media
            ? (String(media.mediaType || "").toUpperCase().includes("VIDEO")
                ? `<video controls src="${escapeHtml(resolveMediaUrl(media.filePath))}"></video>`
                : `<img src="${escapeHtml(resolveMediaUrl(media.filePath))}" alt="\u5a92\u4f53\u9884\u89c8">`)
            : `<span class="muted">\u9009\u62e9\u6587\u4ef6\u540e\u5373\u53ef\u9884\u89c8</span>`
        }
      </div>
    </div>
  `;

  const fileInput = form.querySelector('input[name="file"]');
  const preview = form.querySelector('#albumFilePreview');
  fileInput.addEventListener('change', () => {
    preview.innerHTML = '';
    const file = fileInput.files[0];
    if (!file) {
      if (media) {
        const fallback = String(media.mediaType || "").toUpperCase().includes("VIDEO")
          ? `<video controls src="${escapeHtml(resolveMediaUrl(media.filePath))}"></video>`
          : `<img src="${escapeHtml(resolveMediaUrl(media.filePath))}" alt="\u5a92\u4f53\u9884\u89c8">`;
        preview.innerHTML = fallback;
      } else {
        preview.innerHTML = '<span class="muted">\u9009\u62e9\u6587\u4ef6\u540e\u5373\u53ef\u9884\u89c8</span>';
      }
      return;
    }
    if (file.type.startsWith('video/')) {
      const video = document.createElement('video');
      video.controls = true;
      video.src = URL.createObjectURL(file);
      preview.appendChild(video);
    } else {
      const img = document.createElement('img');
      img.src = URL.createObjectURL(file);
      img.alt = '\u5a92\u4f53\u9884\u89c8';
      preview.appendChild(img);
    }
  });

  modal.open({
    title: media ? "\u7f16\u8f91\u76f8\u518c\u6761\u76ee" : "\u65b0\u589e\u76f8\u518c\u6761\u76ee",
    content: form,
    submitText: "\u4fdd\u5b58",
    onSubmit: async () => {
      if (!form.reportValidity()) {
        return false;
      }

      const payload = {
        title: form.title.value.trim() || null,
        description: form.description.value.trim() || null,
        takenAt: normalizeDateTimeValue(form.takenAt.value),
        mood: form.mood.value.trim() || null,
        mediaType: media?.mediaType || null,
        filePath: media?.filePath || null,
      };

      const file = fileInput.files[0];
      if (file) {
        const upload = await uploadFile(file);
        payload.mediaType = (upload.mediaType || "").toUpperCase().includes("VIDEO") ? "VIDEO" : "IMAGE";
        payload.filePath = upload.filePath;
      }

      if (media) {
        if (!payload.filePath || !payload.mediaType) {
          notify("\u8bf7\u5148\u9009\u62e9\u9700\u8981\u4e0a\u4f20\u7684\u6587\u4ef6", "error");
          return false;
        }
        await apiRequest(`/api/album/${media.id}`, { method: "PUT", body: payload });
        notify("\u76f8\u518c\u6761\u76ee\u5df2\u66f4\u65b0", "success");
      } else {
        if (!payload.filePath) {
          notify("\u8bf7\u9009\u62e9\u9700\u8981\u4e0a\u4f20\u7684\u6587\u4ef6", "error");
          return false;
        }
        await apiRequest("/api/album", { method: "POST", body: payload });
        notify("\u56de\u5fc6\u5df2\u6536\u85cf", "success");
      }

      await refreshModule("album");
      renderAlbum();
    },
  });
}

async function deleteAlbumMedia(media) {
  if (!confirm("\u786e\u5b9a\u8981\u5220\u9664\u8fd9\u6bb5\u56de\u5fc6\u5417\uff1f")) return;
  try {
    await apiRequest(`/api/album/${media.id}`, { method: "DELETE" });
    notify("\u5df2\u5220\u9664\u76f8\u518c\u6761\u76ee", "success");
    await refreshModule("album");
    renderAlbum();
  } catch (error) {
    notify(error.message || "\u5220\u9664\u5931\u8d25", "error");
  }
}
function handlePreviewClick(event) {
  const trigger = event.target.closest("[data-preview]");
  if (!trigger) {
    return;
  }
  const url = trigger.getAttribute("data-preview");
  if (!url) {
    return;
  }
  event.preventDefault();
  event.stopPropagation();
  const type = trigger.getAttribute("data-media-type") || "image";
  const caption = trigger.getAttribute("data-preview-caption") || "";
  previewer.open(url, type, caption);
}

function createMediaViewer() {
  const overlay = document.createElement("div");
  overlay.className = "media-viewer hidden";
  overlay.setAttribute("tabindex", "-1");
  overlay.innerHTML = `
    <div class="media-viewer__mask"></div>
    <div class="media-viewer__panel">
      <button type="button" class="media-viewer__close" aria-label="\u5173\u95ed\u9884\u89c8">&times;</button>
      <div class="media-viewer__body"></div>
      <div class="media-viewer__caption"></div>
    </div>
  `;
  document.body.appendChild(overlay);
  const body = overlay.querySelector(".media-viewer__body");
  const captionNode = overlay.querySelector(".media-viewer__caption");
  const closeBtn = overlay.querySelector(".media-viewer__close");
  const mask = overlay.querySelector(".media-viewer__mask");

  function close() {
    overlay.classList.add("hidden");
    body.innerHTML = "";
    captionNode.textContent = "";
  }

  closeBtn.addEventListener("click", close);
  mask.addEventListener("click", close);
  overlay.addEventListener("keydown", (event) => {
    if (event.key === "Escape") {
      close();
    }
  });

  return {
    open(url, type, caption) {
      body.innerHTML = "";
      if (type === "video") {
        const video = document.createElement("video");
        video.controls = true;
        video.src = url;
        video.setAttribute("playsinline", "");
        video.setAttribute("preload", "metadata");
        body.appendChild(video);
      } else {
        const img = document.createElement("img");
        img.src = url;
        img.alt = caption || "\u5a92\u4f53\u9884\u89c8";
        body.appendChild(img);
      }
      captionNode.textContent = caption || "";
      overlay.classList.remove("hidden");
      overlay.focus();
    },
    close,
  };
}

function createModal() {
  const overlay = document.createElement("div");
  overlay.className = "modal-overlay hidden";
  overlay.innerHTML = `
    <div class="modal" role="dialog" aria-modal="true">
      <div class="modal-header">
        <h2 class="modal-title"></h2>
        <button type="button" class="modal-close" aria-label="\u5173\u95ed\u5f39\u7a97">\u00d7</button>
      </div>
      <div class="modal-body"></div>
      <div class="modal-footer">
        <button type="button" class="btn-secondary modal-cancel">\u53d6\u6d88</button>
        <button type="button" class="btn-primary modal-submit">\u4fdd\u5b58</button>
      </div>
    </div>
  `;
  document.body.appendChild(overlay);

  const titleEl = overlay.querySelector(".modal-title");
  const bodyEl = overlay.querySelector(".modal-body");
  const submitBtn = overlay.querySelector(".modal-submit");
  const cancelBtn = overlay.querySelector(".modal-cancel");
  const closeBtn = overlay.querySelector(".modal-close");
  const submitBaseClass = submitBtn.className;
  const cancelBaseClass = cancelBtn.className;
  let submitHandler = null;
  let cancelHandler = null;

  function close() {
    overlay.classList.add("hidden");
    overlay.setAttribute("aria-hidden", "true");
    bodyEl.innerHTML = "";
    submitHandler = null;
    cancelHandler = null;
    submitBtn.className = submitBaseClass;
    cancelBtn.className = cancelBaseClass;
  }

  function triggerCancel() {
    const handler = cancelHandler;
    cancelHandler = null;
    close();
    if (typeof handler === "function") {
      handler();
    }
  }

  function setLoading(loading) {
    submitBtn.disabled = loading;
    cancelBtn.disabled = loading;
    submitBtn.classList.toggle("loading", loading);
  }

  closeBtn.addEventListener("click", triggerCancel);
  cancelBtn.addEventListener("click", triggerCancel);
  overlay.addEventListener("click", (event) => {
    if (event.target === overlay) {
      triggerCancel();
    }
  });

  submitBtn.addEventListener("click", async () => {
    if (!submitHandler) {
      close();
      return;
    }
    try {
      setLoading(true);
      const shouldClose = await submitHandler();
      if (shouldClose !== false) {
        cancelHandler = null;
        close();
      }
    } catch (error) {
      notify(error.message || "\u64cd\u4f5c\u5931\u8d25", "error");
    } finally {
      setLoading(false);
    }
  });

  return {
    open({
      title,
      content,
      submitText = "\u4fdd\u5b58",
      cancelText = "\u53d6\u6d88",
      onSubmit,
      onCancel,
      submitClassName,
      cancelClassName,
      hideCancel = false,
      hideSubmit = false,
    }) {
      titleEl.textContent = title || "";
      bodyEl.innerHTML = "";
      if (typeof content === "string") {
        bodyEl.innerHTML = content;
      } else if (content instanceof Node) {
        bodyEl.appendChild(content);
      }
      submitHandler = typeof onSubmit === "function" ? onSubmit : null;
      cancelHandler = typeof onCancel === "function" ? onCancel : null;
      submitBtn.textContent = submitText;
      cancelBtn.textContent = cancelText;
      submitBtn.className = submitClassName ? `${submitBaseClass} ${submitClassName}` : submitBaseClass;
      cancelBtn.className = cancelClassName ? `${cancelBaseClass} ${cancelClassName}` : cancelBaseClass;
      submitBtn.style.display = hideSubmit ? "none" : "inline-flex";
      cancelBtn.style.display = hideCancel ? "none" : "inline-flex";
      if (hideCancel) {
        cancelHandler = null;
      }
      overlay.classList.remove("hidden");
      overlay.setAttribute("aria-hidden", "false");
    },
    close,
  };
}

function createToast() {
  const host = document.createElement("div");
  host.className = "toast-host";
  document.body.appendChild(host);

  function push(message, type = "info") {
    const toastNode = document.createElement("div");
    toastNode.className = `toast toast-${type}`;
    toastNode.textContent = message;
    host.appendChild(toastNode);
    requestAnimationFrame(() => {
      toastNode.classList.add("show");
    });
    setTimeout(() => {
      toastNode.classList.remove("show");
      setTimeout(() => toastNode.remove(), 300);
    }, 3200);
  }

  return { push };
}

async function apiRequest(path, options = {}) {
  const { method = "GET", params, body, headers = {} } = options;
  const url = new URL(path, window.location.origin);
  if (params) {
    Object.entries(params).forEach(([key, value]) => {
      if (value !== undefined && value !== null && value !== "") {
        url.searchParams.append(key, value);
      }
    });
  }

  const fetchOptions = {
    method,
    headers: {
      ...headers,
    },
  };

  if (state.token) {
    fetchOptions.headers.Authorization = `Bearer ${state.token}`;
  }

  if (body instanceof FormData) {
    fetchOptions.body = body;
  } else if (body !== undefined) {
    fetchOptions.headers["Content-Type"] = "application/json";
    fetchOptions.body = JSON.stringify(body);
  }

  const response = await fetch(url, fetchOptions);
  if (response.status === 401) {
    localStorage.removeItem(TOKEN_KEY);
    localStorage.removeItem(PROFILE_CACHE_KEY);
    window.location.href = "index.html";
    throw new Error("\u767b\u5f55\u72b6\u6001\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55");
  }

  if (response.status === 204) {
    return null;
  }

  let data = null;
  try {
    data = await response.json();
  } catch (_) {
    data = null;
  }

  if (!response.ok) {
    throw new Error((data && data.message) || `\u8bf7\u6c42\u5931\u8d25\uff1a${response.status}`);
  }

  if (data && data.success === false) {
    throw new Error(data.message || "\u8bf7\u6c42\u5931\u8d25");
  }

  return data?.data ?? data;
}

